Cleaned up several tile properties; added railroad adjusting

This commit is contained in:
Eevee (Evelyn Woods) 2020-12-02 13:54:32 -07:00
parent 72cba627a8
commit f0680ce0c4
8 changed files with 296 additions and 122 deletions

View File

@ -34,6 +34,8 @@ export const DIRECTIONS = {
opposite: 'west',
},
};
// Should match the bit ordering above, and CC2's order
export const DIRECTION_ORDER = ['north', 'east', 'south', 'west'];
// TODO cc2 order is: swivel, thinwalls, canopy (and yes you can have them all in the same tile)
export const DRAW_LAYERS = {

View File

@ -1,11 +1,10 @@
import { TransientOverlay } from './main-base.js';
import { mk } from './util.js';
import { mk, mk_svg } from './util.js';
// FIXME could very much stand to have a little animation when appearing
class TileEditorOverlay extends TransientOverlay {
constructor(conductor) {
let root = mk('form.editor-popup-tile-editor');
root.append(mk('span.popup-chevron'));
super(conductor, root);
this.editor = conductor.editor;
this.tile = null;
@ -24,34 +23,41 @@ class LetterTileEditor extends TileEditorOverlay {
constructor(conductor) {
super(conductor);
this.root.append(mk('h3', "Letter tile"));
let list = mk('ol.editor-letter-tile-picker');
this.root.append(list);
this.glyph_elements = {};
for (let c = 32; c < 128; c++) {
let glyph = String.fromCharCode(c);
let add = glyph => {
let input = mk('input', {type: 'radio', name: 'glyph', value: glyph});
this.glyph_elements[glyph] = input;
let item = mk('li', mk('label', input, mk('span.-glyph', glyph)));
list.append(item);
};
let arrows = ["⬆", "➡", "⬇", "⬅"];
for (let c = 32; c < 96; c++) {
let glyph = String.fromCharCode(c);
add(glyph);
// Add the arrows to the ends of the rows
if (c % 16 === 15) {
add(arrows[(c - 47) / 16]);
}
}
list.addEventListener('change', ev => {
let glyph = this.root.elements['glyph'].value;
if (this.tile) {
this.tile.ascii_code = glyph.charCodeAt(0);
// FIXME should be able to mark tiles as dirty, also this is sure a mouthful
this.conductor.editor.renderer.draw();
this.tile.overlaid_glyph = this.root.elements['glyph'].value;
this.editor.mark_tile_dirty(this.tile);
}
});
}
edit_tile(tile) {
super.edit_tile(tile);
this.root.elements['glyph'].value = String.fromCharCode(tile.ascii_code);
this.root.elements['glyph'].value = tile.overlaid_glyph;
}
static configure_tile_defaults(tile) {
tile.ascii_code = 32;
tile.type.populate_defaults(tile);
}
}
@ -59,29 +65,110 @@ class HintTileEditor extends TileEditorOverlay {
constructor(conductor) {
super(conductor);
this.root.append(mk('h3', "Hint text"));
this.text = mk('textarea.editor-hint-tile-text');
this.root.append(this.text);
this.text.addEventListener('input', ev => {
if (this.tile) {
this.tile.specific_hint = this.text.value;
this.tile.hint_text = this.text.value;
}
});
}
edit_tile(tile) {
super.edit_tile(tile);
this.text.value = tile.specific_hint ?? "";
this.text.value = tile.hint_text ?? "";
}
static configure_tile_defaults(tile) {
tile.specific_hint = "";
tile.hint_text = "";
}
}
class RailroadTileEditor extends TileEditorOverlay {
constructor(conductor) {
super(conductor);
let svg_icons = [];
for (let center of [[16, 0], [16, 16], [0, 16], [0, 0]]) {
let symbol = mk_svg('svg', {viewBox: '0 0 16 16'},
mk_svg('circle', {cx: center[0], cy: center[1], r: 3}),
mk_svg('circle', {cx: center[0], cy: center[1], r: 13}),
);
svg_icons.push(symbol);
}
svg_icons.push(mk_svg('svg', {viewBox: '0 0 16 16'},
mk_svg('rect', {x: -2, y: 3, width: 20, height: 10}),
));
svg_icons.push(mk_svg('svg', {viewBox: '0 0 16 16'},
mk_svg('rect', {x: 3, y: -2, width: 10, height: 20}),
));
this.root.append(mk('h3', "Tracks"));
let track_list = mk('ul.editor-railroad-tile-tracks');
// Shown as two rows, this puts the straight parts first and the rest in a circle
let track_order = [4, 1, 2, 5, 0, 3];
for (let i of track_order) {
let input = mk('input', {type: 'checkbox', name: 'track', value: i});
track_list.append(mk('li', mk('label', input, svg_icons[i])));
}
track_list.addEventListener('change', ev => {
if (this.tile) {
let bit = 1 << ev.target.value;
if (ev.target.checked) {
this.tile.tracks |= bit;
}
else {
this.tile.tracks &= ~bit;
}
this.editor.mark_tile_dirty(this.tile);
}
});
this.root.append(track_list);
this.root.append(mk('h3', "Switch"));
let switch_list = mk('ul.editor-railroad-tile-tracks.--switch');
for (let i of track_order) {
let input = mk('input', {type: 'radio', name: 'switch', value: i});
switch_list.append(mk('li', mk('label', input, svg_icons[i].cloneNode(true))));
}
// TODO if they remove a track it should change the switch
// TODO if they pick a track that's missing it should add it
switch_list.addEventListener('change', ev => {
if (this.tile) {
this.tile.track_switch = ev.target.value;
this.editor.mark_tile_dirty(this.tile);
}
});
this.root.append(switch_list);
// TODO need a way to set no actor at all
// TODO initial actor facing (maybe only if there's an actor in the cell)
}
edit_tile(tile) {
super.edit_tile(tile);
for (let input of this.root.elements['track']) {
input.checked = !! (tile.tracks & (1 << input.value));
}
if (tile.track_switch === null) {
this.root.elements['switch'].value = '';
}
else {
this.root.elements['switch'].value = tile.track_switch;
}
}
static configure_tile_defaults(tile) {
}
}
export const TILES_WITH_PROPS = {
floor_letter: LetterTileEditor,
hint: HintTileEditor,
railroad: RailroadTileEditor,
// TODO various wireable tiles
// TODO initial value of counter
// TODO cloner arrows

View File

@ -1,4 +1,4 @@
import { DIRECTIONS } from './defs.js';
import { DIRECTIONS, DIRECTION_ORDER } from './defs.js';
import * as format_base from './format-base.js';
import TILE_TYPES from './tiletypes.js';
import * as util from './util.js';
@ -374,11 +374,28 @@ const TILE_ENCODING = {
modifier: {
_parts: ['ne', 'se', 'sw', 'ne', 'ew', 'ns'],
decode(tile, mask) {
tile.railroad_bits = mask;
// Leave the track parts alone as a bitmask; the type has a list of them
tile.tracks = mask & 0x3f;
// Check for a switch, which is a bit number in the above mask
if (mask & 0x40) {
tile.track_switch = (mask >> 8) & 0x0f;
}
else {
tile.track_switch = null;
}
// Initial actor facing is in the highest nybble
tile.entered_direction = (mask >> 12) & 0x03;
},
encode(tile) {
// TODO
return 0;
let ret = tile.tracks & 0x3f;
if (tile.track_switch !== null) {
ret |= 0x40;
ret |= tile.track_switch << 8;
}
if (tile.entered_direction) {
ret |= DIRECTION_ORDER.indexOf(tile.entered_direction) << 12;
}
return ret;
},
},
},
@ -519,10 +536,25 @@ const TILE_ENCODING = {
name: 'floor_letter',
modifier: {
decode(tile, ascii_code) {
tile.ascii_code = ascii_code;
if (ascii_code < 28 || ascii_code >= 96) {
// Invalid
tile.overlaid_glyph = "?";
}
else if (ascii_code < 32) {
// Arrows are stored goofily
tile.overlaid_glyph = ["⬆", "➡", "⬇", "⬅"][ascii_code - 28];
}
else {
tile.overlaid_glyph = String.fromCharCode(ascii_code);
}
},
encode(tile) {
return tile.ascii_code;
let arrow_index = ["⬆", "➡", "⬇", "⬅"].indexOf(tile.overlaid_glyph);
if (arrow_index >= 0) {
return arrow_index + 28;
}
return tile.overlaid_glyph.charCodeAt(0);
},
},
},
@ -798,11 +830,10 @@ 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].
// For my purposes, extra hints are associated with the
// individual tiles, so we'll map those later
[level.comment, ...extra_hints] = str.split(/^\[CLUE\]$/mg);
// 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);
}
continue;
}
@ -1018,13 +1049,14 @@ export function parse_level(buf, number = 1) {
}
// Connect extra hints
let h = 0;
for (let tile of hint_tiles) {
if (h > extra_hints.length)
break;
tile.specific_hint = extra_hints[h];
h++;
for (let [i, tile] of hint_tiles.entries()) {
if (i < extra_hints.length) {
tile.hint_text = extra_hints[i];
}
else {
// Fall back to regular hint
tile.hint_text = null;
}
}
return level;

View File

@ -297,7 +297,7 @@ export class Level {
let tile = Tile.from_template(template_tile);
if (tile.type.is_hint) {
// Copy over the tile-specific hint, if any
tile.specific_hint = template_tile.specific_hint ?? null;
tile.hint_text = template_tile.hint_text ?? null;
}
if (tile.type.is_power_source) {
@ -390,7 +390,7 @@ export class Level {
tile.type.on_ready(tile, this);
}
if (cell === this.player.cell && tile.type.is_hint) {
this.hint_shown = tile.specific_hint ?? this.stored_level.hint;
this.hint_shown = tile.hint_text ?? this.stored_level.hint;
}
}
}
@ -1019,7 +1019,7 @@ export class Level {
}
if (actor === this.player && tile.type.is_hint) {
this.hint_shown = tile.specific_hint ?? this.stored_level.hint;
this.hint_shown = tile.hint_text ?? this.stored_level.hint;
}
}

View File

@ -284,7 +284,7 @@ class TrackOperation extends DrawOperation {
// Get the corresponding bit
let bit = null;
for (let [i, track] of TILE_TYPES['railroad']._track_order.entries()) {
for (let [i, track] of TILE_TYPES['railroad'].track_order.entries()) {
if ((track[0] === this.entry_direction && track[1] === exit_direction) ||
(track[1] === this.entry_direction && track[0] === exit_direction))
{
@ -300,10 +300,12 @@ class TrackOperation extends DrawOperation {
let cell = this.cell(prevx, prevy);
let terrain = cell[0];
if (terrain.type.name === 'railroad') {
terrain.railroad_bits |= bit;
terrain.tracks |= bit;
}
else {
terrain = { type: TILE_TYPES['railroad'], railroad_bits: bit };
terrain = { type: TILE_TYPES['railroad'] };
terrain.type.populate_defaults(terrain);
terrain.tracks |= bit;
this.editor.place_in_cell(prevx, prevy, terrain);
}
@ -364,17 +366,19 @@ class AdjustOperation extends MouseOperation {
for (let tile of cell) {
// Rotate railroads, which are a bit complicated
if (tile.type.name === 'railroad') {
let new_bits = 0;
for (let [i, new_bit] of [1, 2, 3, 0, 5, 4].entries()) {
if (tile.railroad_bits & (1 << i)) {
new_bits |= (1 << new_bit);
let new_tracks = 0;
let rotated_tracks = [1, 2, 3, 0, 5, 4];
for (let [i, new_bit] of rotated_tracks.entries()) {
if (tile.tracks & (1 << i)) {
new_tracks |= (1 << new_bit);
}
}
if (tile.railroad_bits & 0x40) {
new_bits |= 0x40;
tile.tracks = new_tracks;
if (tile.switch_track !== null) {
tile.switch_track = rotated_tracks[tile.switch_track];
}
// TODO high byte also
tile.railroad_bits = new_bits;
tile.entered_direction = DIRECTIONS[tile.entered_direction].right;
}
// TODO also directional blocks
@ -832,7 +836,6 @@ export class Editor extends PrimaryView {
this.selected_tile_el.addEventListener('click', ev => {
if (this.palette_selection && TILES_WITH_PROPS[this.palette_selection.type.name]) {
// FIXME use tile bounds
// FIXME redraw the tile after editing
this.open_tile_prop_overlay(this.palette_selection, ev.clientX, ev.clientY);
}
});
@ -973,11 +976,6 @@ export class Editor extends PrimaryView {
name = tile.type.name;
}
// FIXME should redraw in an existing canvas
this.selected_tile_el.textContent = '';
// FIXME should draw the actual tile!!
this.selected_tile_el.append(this.renderer.create_tile_type_canvas(name, tile));
if (this.palette_selection) {
let entry = this.palette[this.palette_selection.type.name];
if (entry) {
@ -989,6 +987,8 @@ export class Editor extends PrimaryView {
this.palette[name].classList.add('--selected');
}
this.mark_tile_dirty(tile);
// Some tools obviously don't work with a palette selection, in which case changing tiles
// should default you back to the pencil
if (this.current_tool === 'adjust') {
@ -996,6 +996,18 @@ export class Editor extends PrimaryView {
}
}
mark_tile_dirty(tile) {
// TODO partial redraws! until then, redraw everything
if (tile === this.palette_selection) {
// FIXME should redraw in an existing canvas
this.selected_tile_el.textContent = '';
this.selected_tile_el.append(this.renderer.create_tile_type_canvas(tile.type.name, tile));
}
else {
this.renderer.draw();
}
}
is_in_bounds(x, y) {
return 0 <= x && x < this.stored_level.size_x && 0 <= y && y < this.stored_level.size_y;
}
@ -1051,13 +1063,13 @@ export class Editor extends PrimaryView {
// Horizontal position: centered, but kept within the screen
let left;
let margin = 8; // prefer to not quite touch the edges
let halfwidth = root.offsetWidth / 2 + margin;
if (document.body.clientWidth / 2 < halfwidth) {
if (document.body.clientWidth < root.offsetWidth + margin * 2) {
// It doesn't fit on the screen at all, so there's nothing we can do; just center it
left = (document.body.clientWidth - root.offsetWidth) / 2;
}
else {
left = Math.max(margin, Math.min(document.body.clientWidth - halfwidth, x0 - halfwidth));
left = Math.max(margin, Math.min(document.body.clientWidth - root.offsetWidth - margin,
x0 - root.offsetWidth / 2));
}
root.style.left = `${left}px`;
root.style.setProperty('--chevron-offset', `${x0 - left}px`);

View File

@ -63,18 +63,25 @@ export const CC2_TILESET_LAYOUT = {
wall_invisible: [0, 2],
wall_appearing: [0, 2],
wall: [1, 2],
floor_letter: [2, 2],
'floor_letter#ascii': {
x0: 0,
y0: 0,
width: 16,
height: 1,
},
'floor_letter#arrows': {
north: [14, 31],
east: [14.5, 31],
south: [15, 31],
west: [15.5, 31],
floor_letter: {
special: 'letter',
base: [2, 2],
letter_glyphs: {
// Arrows
"⬆": [14, 31],
"➡": [14.5, 31],
"⬇": [15, 31],
"⬅": [15.5, 31],
},
letter_ranges: [{
// ASCII text (only up through uppercase)
range: [32, 96],
x0: 0,
y0: 0,
w: 0.5,
h: 0.5,
columns: 32,
}],
},
thief_tools: [3, 2],
socket: [4, 2],
@ -830,6 +837,31 @@ export class Tileset {
blit(coords[0], coords[1], ...mask);
}
_draw_letter(drawspec, tile, tic, blit) {
this._draw_standard(drawspec.base, tile, tic, blit);
let glyph = tile.overlaid_glyph;
if (drawspec.letter_glyphs[glyph]) {
let [x, y] = drawspec.letter_glyphs[glyph];
// XXX size is hardcoded here, but not below, meh
blit(x, y, 0, 0, 0.5, 0.5, 0.25, 0.25);
}
else {
// Look for a range
let u = glyph.charCodeAt(0);
for (let rangedef of drawspec.letter_ranges) {
if (rangedef.range[0] <= u && u < rangedef.range[1]) {
let t = u - rangedef.range[0];
let x = rangedef.x0 + rangedef.w * (t % rangedef.columns);
let y = rangedef.y0 + rangedef.h * Math.floor(t / rangedef.columns);
blit(x, y, 0, 0, rangedef.w, rangedef.h,
(1 - rangedef.w) / 2, (1 - rangedef.h) / 2);
break;
}
}
}
}
_draw_logic_gate(drawspec, tile, tic, blit) {
// Layer 1: wiring state
// Always draw the unpowered wire base
@ -876,15 +908,15 @@ export class Tileset {
let visible_parts = [];
let topmost_part = null;
for (let [i, part] of part_order.entries()) {
if (tile && (tile.railroad_bits & (1 << i))) {
if ((tile.railroad_bits >> 8) === i) {
if (tile && (tile.tracks & (1 << i))) {
if (tile.track_switch === i) {
topmost_part = part;
}
visible_parts.push(part);
}
}
let has_switch = (tile && (tile.railroad_bits & 0x40));
let has_switch = (tile && tile.track_switch !== null);
for (let part of visible_parts) {
this._draw_standard(drawspec.railroad_ties[part], tile, tic, blit);
}
@ -929,7 +961,11 @@ export class Tileset {
// TODO shift everything to use this style, this is ridiculous
if (drawspec.special) {
if (drawspec.special === 'logic-gate') {
if (drawspec.special === 'letter') {
this._draw_letter(drawspec, tile, tic, blit);
return;
}
else if (drawspec.special === 'logic-gate') {
this._draw_logic_gate(drawspec, tile, tic, blit);
return;
}
@ -1144,36 +1180,6 @@ export class Tileset {
}
blit(x, y, x0, y0, x1 - x0, y1 - y0);
}
// Special behavior for special objects
// TODO? hardcode this less?
if (name === 'floor_letter' && tile) {
let n = tile.ascii_code - 32;
let scale = 0.5;
let sx, sy;
if (n < 0) {
// Arrows
if (n < -4) {
// Default to south
n = -2;
}
let direction = ['north', 'east', 'south', 'west'][n + 4];
[sx, sy] = this.layout['floor_letter#arrows'][direction];
}
else {
// ASCII text (only up through uppercase)
let letter_spec = this.layout['floor_letter#ascii'];
if (n > letter_spec.width / scale * letter_spec.height / scale) {
n = 0;
}
let w = letter_spec.width / scale;
sx = (letter_spec.x0 + n % w) * scale;
sy = (letter_spec.y0 + Math.floor(n / w)) * scale;
}
let offset = (1 - scale) / 2;
blit(sx, sy, 0, 0, 0.5, 0.5, offset, offset);
}
}
_rotate(direction, x0, y0, x1, y1) {

View File

@ -90,6 +90,9 @@ const TILE_TYPES = {
},
floor_letter: {
draw_layer: DRAW_LAYERS.terrain,
populate_defaults(me) {
me.overlaid_glyph = "?";
},
},
// TODO possibly this should be a single tile
floor_custom_green: {
@ -316,7 +319,7 @@ const TILE_TYPES = {
// Railroad
railroad: {
draw_layer: DRAW_LAYERS.terrain,
_track_order: [
track_order: [
['north', 'east'],
['south', 'east'],
['south', 'west'],
@ -324,15 +327,12 @@ const TILE_TYPES = {
['east', 'west'],
['north', 'south'],
],
// FIXME railroad_bits sucks, split into some useful variables
on_ready(me) {
// If there's already an actor on top of us, assume it entered the way it's already
// facing (which may be illegal, in which case it can't leave)
// FIXME wrong! > Yeah, so in the high byte, the low nibble encodes the active track. What's missing is that the high nibble encodes a direction value, which is required when a mob starts out on top of the track: it represents the direction the mob was going when it entered the tile, which controls which way it can go on the track.
let actor = me.cell.get_actor();
if (actor) {
me.entered_direction = actor.direction;
}
populate_defaults(me) {
me.tracks = 0; // bitmask of bits 0-5, corresponding to track order above
me.track_switch = null; // null, or 0-5 indicating the active switched track
// If there's already an actor on us, it's treated as though it entered the tile moving
// in this direction, which is given in the save file and defaults to zero i.e. north
me.entered_direction = 'north';
},
// TODO feel like "ignores" was the wrong idea and there should just be some magic flags for
// particular objects that can be immune to. or maybe those objects should have their own
@ -345,28 +345,28 @@ const TILE_TYPES = {
return true;
},
*_iter_tracks(me) {
let order = me.type._track_order;
if (me.railroad_bits & 0x40) {
let order = me.type.track_order;
if (me.track_switch !== null) {
// FIXME what happens if the "top" track is not actually a valid track???
yield order[me.railroad_bits >> 8];
yield order[me.track_switch];
}
else {
for (let [i, track] of order.entries()) {
if (me.railroad_bits & (1 << i)) {
if (me.tracks & (1 << i)) {
yield track;
}
}
}
},
_switch_track(me, level) {
if (me.railroad_bits & 0x40) {
let current = me.railroad_bits >> 8;
for (let i = 0, l = me.type._track_order.length; i < l; i++) {
if (me.track_switch !== null) {
let current = me.track_switch;
for (let i = 0, l = me.type.track_order.length; i < l; i++) {
current = (current + 1) % l;
if (me.railroad_bits & (1 << current))
if (me.tracks & (1 << current))
break;
}
level._set_tile_prop(me, 'railroad_bits', (current << 8) | (me.railroad_bits & 0xff));
level._set_tile_prop(me, 'track_switch', current);
}
},
has_opening(me, direction) {
@ -1817,6 +1817,9 @@ const TILE_TYPES = {
draw_layer: DRAW_LAYERS.terrain,
is_hint: true,
blocks_collision: COLLISION.block_cc1 | COLLISION.monster_solid,
populate_defaults(me) {
me.hint_text = null; // optional, may use level's hint instead
},
},
socket: {
draw_layer: DRAW_LAYERS.terrain,

View File

@ -1077,7 +1077,16 @@ form.editor-popup-tile-editor {
form.editor-popup-tile-editor.--above {
margin-top: -1em;
}
.popup-chevron {
form.editor-popup-tile-editor h3 {
border-bottom: 1px dotted #606060;
}
form.editor-popup-tile-editor * + h3 {
margin-top: 0.25em;
}
/* Use ::before for a chevron pointing at the tile in question */
form.editor-popup-tile-editor::before {
content: '';
display: block;
position: absolute;
border: 1em solid transparent;
left: var(--chevron-offset);
@ -1088,7 +1097,7 @@ form.editor-popup-tile-editor.--above {
border-bottom-color: white;
filter: drop-shadow(0 -1px 0 black);
}
form.editor-popup-tile-editor.--above .popup-chevron {
form.editor-popup-tile-editor.--above::before {
top: auto;
bottom: -1em;
border: 1em solid transparent;
@ -1100,7 +1109,7 @@ form.editor-popup-tile-editor.--above .popup-chevron {
* characters are preceded by radio buttons, which we hide for simplicity */
ol.editor-letter-tile-picker {
display: grid;
grid: auto-flow 1.5em / repeat(16, 1.5em);
grid: auto-flow 1.5em / repeat(17, 1.5em);
text-align: center;
font-family: monospace;
}
@ -1123,5 +1132,28 @@ textarea.editor-hint-tile-text {
height: 20vh;
min-width: 15rem;
min-height: 5rem;
border: none;
font-family: serif;
}
/* Railroad tracks are... complicated */
ul.editor-railroad-tile-tracks {
display: grid;
grid: auto-flow 3em / repeat(3, 3em);
gap: 0.25em;
}
ul.editor-railroad-tile-tracks input {
display: none;
}
ul.editor-railroad-tile-tracks svg {
display: block;
width: 3em;
fill: none;
stroke: #c0c0c0;
stroke-width: 2;
}
ul.editor-railroad-tile-tracks input:checked + svg {
stroke: hsl(225, 90%, 50%);
}
ul.editor-railroad-tile-tracks.--switch input:checked + svg {
stroke: hsl(15, 90%, 50%);
}