diff --git a/js/format-base.js b/js/format-base.js index a01f1a7..384d524 100644 --- a/js/format-base.js +++ b/js/format-base.js @@ -1,6 +1,10 @@ +import { LAYERS } from './defs.js'; import * as util from './util.js'; export class StoredCell extends Array { + constructor() { + super(LAYERS.MAX); + } } export class Replay { diff --git a/js/format-c2g.js b/js/format-c2g.js index 55fee9a..269eb7f 100644 --- a/js/format-c2g.js +++ b/js/format-c2g.js @@ -1073,10 +1073,12 @@ export function parse_level(buf, number = 1) { let mask = bytes[p]; p++; if (mask & 0x10) { - cell.push({type: TILE_TYPES['canopy']}); + let type = TILE_TYPES['canopy']; + cell[type.layer] = {type}; } if (mask & 0x0f) { - cell.push({type: TILE_TYPES['thin_walls'], edges: mask & 0x0f}); + let type = TILE_TYPES['thin_walls']; + cell[type.layer] = {type, edges: mask & 0x0f}; } // Skip the rest of the loop. That means we don't handle any of the other // special behavior below, but neither thin walls nor canopies should use @@ -1091,7 +1093,7 @@ export function parse_level(buf, number = 1) { let type = TILE_TYPES[name]; if (!type) console.error(name, spec); let tile = {type}; - cell.push(tile); + cell[type.layer] = tile; if (spec.modifier) { spec.modifier.decode(tile, modifier); } @@ -1100,7 +1102,8 @@ export function parse_level(buf, number = 1) { // TODO this should go on the bottom // TODO we should sort and also only allow one thing per layer if (spec.dummy_terrain) { - cell.push({type: TILE_TYPES[spec.dummy_terrain]}); + let type = TILE_TYPES[spec.dummy_terrain]; + cell[type.layer] = {type}; } if (type.is_required_chip) { @@ -1125,7 +1128,6 @@ export function parse_level(buf, number = 1) { if (! spec.has_next) break; } - cell.reverse(); level.linear_cells.push(cell); } } @@ -1389,15 +1391,12 @@ export function synthesize_level(stored_level) { map_view = new DataView(map_bytes.buffer); } - // TODO complain if duplicates on a layer let dummy_terrain_tile = null; let handled_thin_walls = false; - // FIXME sort first, otherwise the canopy assumption that thin walls are immediately below - // breaks! maybe just fix that also - for (let i = cell.length - 1; i >= 0; i--) { + for (let i = LAYERS.MAX - 1; i >= 0; i--) { let tile = cell[i]; - // FIXME does not yet support canopy or thin walls >:S - let spec = REVERSE_TILE_ENCODING[tile.type.name]; + if (! tile) + continue; if (tile.type.name === 'canopy' || tile.type.name === 'thin_walls') { // These two tiles are encoded together despite being on different layers. If we @@ -1410,9 +1409,7 @@ export function synthesize_level(stored_level) { let canopy, thin_walls; if (tile.type.name === 'canopy') { canopy = tile; - if (i > 0 && cell[i - 1].type.name === 'thin_walls') { - thin_walls = cell[i - 1]; - } + thin_walls = cell[LAYERS.thin_wall]; } else { thin_walls = tile; @@ -1432,6 +1429,8 @@ export function synthesize_level(stored_level) { continue; } + let spec = REVERSE_TILE_ENCODING[tile.type.name]; + // Handle the swivel, a tile that draws as an overlay but is stored like terrain. In a // level, it has two parts: the swivel itself, and a dummy swivel_floor terrain which is // unencodable. To encode that, we notice when we hit a swivel (which happens first), diff --git a/js/format-dat.js b/js/format-dat.js index f33d224..4cc004c 100644 --- a/js/format-dat.js +++ b/js/format-dat.js @@ -250,14 +250,15 @@ function parse_level(bytes, number) { // pgchip grants directions to ice blocks on cloners by putting a clone block // beneath them instead if (l === 1 && 0x0e <= tile_byte && tile_byte <= 0x11 && - cell[0] && cell[0].type.name === 'ice_block') + cell[LAYER.actor] && cell[LAYER.actor].type.name === 'ice_block') { - cell[0].direction = extra.direction; - cell.unshift({type: TILE_TYPES['cloner']}); + cell[LAYER.actor].direction = extra.direction; + let type = TILE_TYPES['cloner']; + cell[type.layer] = {type}; continue; } - cell.unshift({...tile}); + cell[tile.type.layer] = {...tile}; } } if (c !== 1024) @@ -266,9 +267,8 @@ function parse_level(bytes, number) { // Fix the "floor/empty" nonsense here by adding floor to any cell with no terrain on bottom for (let cell of level.linear_cells) { - if (cell.length === 0 || cell[0].type.layer !== LAYERS.terrain) { - // No terrain; insert a floor - cell.unshift({ type: TILE_TYPES['floor'] }); + if (! cell[LAYER.terrain]) { + cell[LAYER.terrain] = { type: TILE_TYPES['floor'] }; } // TODO we could also deal with weird cases where there's terrain /on top of/ something // else: things underwater, the quirk where a glider will erase the item underneath... diff --git a/js/game.js b/js/game.js index 45861f3..201661f 100644 --- a/js/game.js +++ b/js/game.js @@ -453,9 +453,10 @@ export class Level extends LevelInterface { let stored_cell = this.stored_level.linear_cells[n]; n++; - - // FIXME give this same treatment to stored cells (otherwise the editor is fucked) for (let template_tile of stored_cell) { + if (! template_tile) + continue; + let tile = Tile.from_template(template_tile); if (tile.type.is_hint) { // Copy over the tile-specific hint, if any diff --git a/js/main-editor.js b/js/main-editor.js index 6a36511..fc25df0 100644 --- a/js/main-editor.js +++ b/js/main-editor.js @@ -444,7 +444,13 @@ class PencilOperation extends DrawOperation { if (this.modifier === 'ctrl') { let cell = this.cell(x, y); if (cell) { - this.editor.select_palette(cell[cell.length - 1]); + // Pick the topmost thing + for (let layer = LAYERS.MAX - 1; layer >= 0; layer--) { + if (cell[layer]) { + this.editor.select_palette(cell[layer]); + break; + } + } } return; } @@ -455,8 +461,10 @@ class PencilOperation extends DrawOperation { let cell = this.cell(x, y); if (this.modifier === 'shift') { // Aggressive mode: erase the entire cell - cell.length = 0; - cell.push({type: TILE_TYPES.floor}); + for (let layer = 0; layer < LAYERS.MAX; layer++) { + cell[layer] = null; + } + cell[LAYERS.terrain] = {type: TILE_TYPES.floor}; } else if (template) { // Erase whatever's on the same layer @@ -470,10 +478,12 @@ class PencilOperation extends DrawOperation { if (this.modifier === 'shift') { // Aggressive mode: erase whatever's already in the cell let cell = this.cell(x, y); - cell.length = 0; + for (let layer = 0; layer < LAYERS.MAX; layer++) { + cell[layer] = null; + } let type = this.editor.palette_selection.type; if (type.layer !== LAYERS.terrain) { - cell.push({type: TILE_TYPES.floor}); + cell[LAYERS.terrain] = {type: TILE_TYPES.floor}; } this.editor.place_in_cell(x, y, template); } @@ -593,16 +603,16 @@ class ForceFloorOperation extends DrawOperation { // had some kind of force floor if (i === 2) { let prevcell = this.editor.cell(prevx, prevy); - if (prevcell[0].type.name.startsWith('force_floor_')) { - prevcell[0].type = TILE_TYPES[name]; + if (prevcell[LAYERS.terrain].type.name.startsWith('force_floor_')) { + prevcell[LAYERS.terrain].type = TILE_TYPES[name]; } } // Drawing a loop with force floors creates ice (but not in the previous // cell, obviously) let cell = this.editor.cell(x, y); - if (cell[0].type.name.startsWith('force_floor_') && - cell[0].type.name !== name) + if (cell[LAYERS.terrain].type.name.startsWith('force_floor_') && + cell[LAYERS.terrain].type.name !== name) { name = 'ice'; } @@ -732,9 +742,8 @@ class WireOperation extends DrawOperation { } let bit = DIRECTIONS[direction].bit; - for (let tile of cell) { - if (tile.type.name !== 'floor') - continue; + let terrain = cell[LAYERS.terrain]; + if (terrain.type.name === 'floor') { if (this.alt_mode) { tile.wire_tunnel_directions &= ~bit; } @@ -846,6 +855,8 @@ class WireOperation extends DrawOperation { let cell = this.cell(x, y); for (let tile of Array.from(cell).reverse()) { // TODO probably a better way to do this + if (! tile) + continue; if (['floor', 'steel', 'button_pink', 'button_black', 'teleport_blue', 'teleport_red', 'light_switch_on', 'light_switch_off', 'circuit_block'].indexOf(tile.type.name) < 0) continue; @@ -908,7 +919,7 @@ class AdjustOperation extends MouseOperation { let cell = this.cell(this.gx1, this.gy1); if (this.modifier === 'ctrl') { for (let tile of cell) { - if (TILES_WITH_PROPS[tile.type.name] !== undefined) { + if (tile && TILES_WITH_PROPS[tile.type.name] !== undefined) { // TODO use the tile's bbox, not the mouse position this.editor.open_tile_prop_overlay(tile, this.mx0, this.my0); break; @@ -917,8 +928,10 @@ class AdjustOperation extends MouseOperation { return; } // FIXME implement shift to always target floor, or maybe start from bottom - for (let i = cell.length - 1; i >= 0; i--) { - let tile = cell[i]; + for (let layer = LAYERS.MAX - 1; layer >= 0; layer--) { + let tile = cell[layer]; + if (! tile) + continue; let rotated; if (this.alt_mode) { @@ -2301,7 +2314,7 @@ class Selection { } else { this.floated_cells.push(stored_level.linear_cells[n]); - stored_level.linear_cells[n] = [{type: TILE_TYPES['floor']}]; + stored_level.linear_cells[n] = this.editor._make_cell(x, y); this.editor.mark_cell_dirty(stored_level.linear_cells[n]); } } @@ -2712,9 +2725,11 @@ export class Editor extends PrimaryView { // Level creation, management, and saving - _make_cell() { + _make_cell(x, y) { let cell = new format_base.StoredCell; - cell.push({type: TILE_TYPES['floor']}); + cell.x = x; + cell.y = y; + cell[LAYERS.terrain] = {type: TILE_TYPES.floor}; return cell; } @@ -2725,9 +2740,9 @@ export class Editor extends PrimaryView { 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()); + stored_level.linear_cells.push(this._make_cell(...stored_level.scalar_to_coords(i))); } - stored_level.linear_cells[0].push({type: TILE_TYPES['player'], direction: 'south'}); + stored_level.linear_cells[LAYERS.actor] = {type: TILE_TYPES['player'], direction: 'south'}; return stored_level; } @@ -3098,26 +3113,25 @@ export class Editor extends PrimaryView { let cell = this.cell(x, y); // Replace whatever's on the same layer // TODO should preserve wiring if possible too - for (let i = cell.length - 1; i >= 0; i--) { + let existing_tile = cell[tile.type.layer]; + if (existing_tile) { // If we find a tile of the same type as the one being drawn, see if it has custom // combine behavior (only the case if it came from the palette) - if (cell[i].type === tile.type && + if (existing_tile.type === tile.type && // FIXME this is hacky garbage tile === this.palette_selection && this.palette_selection_from_palette && SPECIAL_PALETTE_BEHAVIOR[tile.type.name] && SPECIAL_PALETTE_BEHAVIOR[tile.type.name].combine_draw) { - SPECIAL_PALETTE_BEHAVIOR[tile.type.name].combine_draw(tile, cell[i]); + SPECIAL_PALETTE_BEHAVIOR[tile.type.name].combine_draw(tile, existing_tile); return; } - if (cell[i].type.layer === tile.type.layer) { - cell.splice(i, 1); - } + // Otherwise erase it + cell[tile.type.layer] = null; } - cell.push(Object.assign({}, tile)); - cell.sort((a, b) => a.type.layer - b.type.layer); + cell[tile.type.layer] = {...tile}; } erase_tile(cell, tile = null) { @@ -3127,29 +3141,28 @@ export class Editor extends PrimaryView { tile = this.palette_selection; } - let layer = tile.type.layer; - for (let i = cell.length - 1; i >= 0; i--) { + let existing_tile = cell[tile.type.layer]; + if (existing_tile) { // If we find a tile of the same type as the one being drawn, see if it has custom // combine behavior (only the case if it came from the palette) - if (cell[i].type === tile.type && + if (existing_tile.type === tile.type && // FIXME this is hacky garbage tile === this.palette_selection && this.palette_selection_from_palette && SPECIAL_PALETTE_BEHAVIOR[tile.type.name] && SPECIAL_PALETTE_BEHAVIOR[tile.type.name].combine_erase) { - let remove = SPECIAL_PALETTE_BEHAVIOR[tile.type.name].combine_erase(tile, cell[i]); + let remove = SPECIAL_PALETTE_BEHAVIOR[tile.type.name].combine_erase(tile, existing_tile); if (! remove) return; } - if (cell[i].type.layer === layer) { - cell.splice(i, 1); - } + // Otherwise erase it + cell[tile.type.layer] = null; } // Don't allow erasing the floor entirely - if (layer === 0) { - cell.unshift({type: TILE_TYPES.floor}); + if (tile.type.layer === LAYERS.terrain) { + cell[LAYERS.terrain] = {type: TILE_TYPES.floor}; } this.mark_cell_dirty(cell); } @@ -3201,7 +3214,7 @@ export class Editor extends PrimaryView { let new_cells = []; for (let y = y0; y < y0 + size_y; y++) { for (let x = x0; x < x0 + size_x; x++) { - new_cells.push(this.cell(x, y) ?? this._make_cell()); + new_cells.push(this.cell(x, y) ?? this._make_cell(x, y)); } }