From bbfa0a6e8fead52011e307ee47a4d1e2dbe85e12 Mon Sep 17 00:00:00 2001 From: "Eevee (Evelyn Woods)" Date: Fri, 28 Aug 2020 07:01:28 -0600 Subject: [PATCH] Split out CC2 tile bytes; stub out enough for Lesson 1 to load; show inventory; implement misc bits --- js/format-c2m.js | 202 +++++++++++++++++-- js/format-dat.js | 17 +- js/main.js | 194 +++++++++++++----- js/tileset.js | 43 ++-- js/tiletypes.js | 506 +++++++++++++++++++++++++---------------------- style.css | 24 ++- 6 files changed, 663 insertions(+), 323 deletions(-) diff --git a/js/format-c2m.js b/js/format-c2m.js index e833d2c..7c8edca 100644 --- a/js/format-c2m.js +++ b/js/format-c2m.js @@ -1,5 +1,156 @@ import * as util from './format-util.js'; -import { TILE_TYPES, CC2_TILE_TYPES } from './tiletypes.js'; +import TILE_TYPES from './tiletypes.js'; + +// TODO assert that direction + next match the tile types +const TILE_ENCODING = { + 0x01: 'floor', + 0x02: 'wall', + 0x03: 'ice', + 0x04: 'ice_sw', + 0x05: 'ice_nw', + 0x06: 'ice_ne', + 0x07: 'ice_se', + 0x08: 'water', + 0x09: 'fire', + 0x0a: 'force_floor_n', + 0x0b: 'force_floor_e', + 0x0c: 'force_floor_s', + 0x0d: 'force_floor_w', + 0x0e: 'green_wall', + 0x0f: 'green_floor', + //0x10: 'teleport_red', + 0x11: 'teleport_blue', + //0x12: 'teleport_yellow', + //0x13: 'teleport_green', + 0x14: 'exit', + //0x15: 'slime', + 0x16: ['player', 'direction', 'next'], + 0x17: ['dirt_block', 'direction', 'next'], + 0x18: ['walker', 'direction', 'next'], + 0x19: ['glider', 'direction', 'next'], + 0x1a: ['ice_block', 'direction', 'next'], + 0x1b: ['thinwall_e', 'next'], + 0x1c: ['thinwall_s', 'next'], + 0x1d: ['thinwall_se', 'next'], + 0x1e: 'gravel', + 0x1f: 'button_green', + 0x20: 'button_blue', + 0x21: ['tank_blue', 'direction', 'next'], + 0x22: 'door_red', + 0x23: 'door_blue', + 0x24: 'door_yellow', + 0x25: 'door_green', + 0x26: ['key_red', 'next'], + 0x27: ['key_blue', 'next'], + 0x28: ['key_yellow', 'next'], + 0x29: ['key_green', 'next'], + 0x2a: ['chip', 'next'], + 0x2b: ['chip_extra', 'next'], + 0x2c: 'socket', + 0x2d: 'popwall', + 0x2e: 'wall_appearing', + 0x2f: 'wall_invisible', + 0x30: 'fake_wall', + 0x31: 'fake_floor', + 0x32: 'dirt', + 0x33: ['bug', 'direction', 'next'], + 0x34: ['paramecium', 'direction', 'next'], + 0x35: ['ball', 'direction', 'next'], + 0x36: ['blob', 'direction', 'next'], + 0x37: ['teeth', 'direction', 'next'], + 0x38: ['fireball', 'direction', 'next'], + 0x39: 'button_red', + 0x3a: 'button_brown', + 0x3b: ['cleats', 'next'], + 0x3c: ['suction_boots', 'next'], + 0x3d: ['fire_boots', 'next'], + 0x3e: ['flippers', 'next'], + 0x3f: 'thief_keys', + 0x40: ['bomb', 'next'], + //0x41: Open trap (unused in main levels) : + 0x42: 'trap', + 0x43: 'cloner', + //0x44: Clone machine : Modifier required, see below + 0x45: 'hint', + //0x46: 'force_floor_all', + // 0x47: 'button_gray', + 0x48: 'swivel_sw', + 0x49: 'swivel_nw', + 0x4a: 'swivel_ne', + 0x4b: 'swivel_se', + // 0x4c: Time bonus : 'next' + // 0x4d: Stopwatch : 'next' + // 0x4e: Transmogrifier : + // 0x4f: Railroad track (Modifier required, see section below) : + // 0x50: Steel wall : + // 0x51: Time bomb : 'next' + // 0x52: Helmet : 'next' + // 0x53: (Unused) : 'direction', 'next' + // 0x54: (Unused) : + // 0x55: (Unused) : + // 0x56: Melinda : 'direction', 'next' + // 0x57: Timid teeth : 'direction', 'next' + // 0x58: Explosion animation (unused in main levels) : 'direction', 'next' + // 0x59: Hiking boots : 'next' + // 0x5a: Male-only sign : + // 0x5b: Female-only sign : + // 0x5c: Inverter gate (N) : Modifier allows other gates, see below + // 0x5d: (Unused) : 'direction', 'next' + // 0x5e: Logic switch (ON) : + // 0x5f: Flame jet (OFF) : + // 0x60: Flame jet (ON) : + // 0x61: Orange button : + // 0x62: Lightning bolt : 'next' + // 0x63: Yellow tank : 'direction', 'next' + // 0x64: Yellow tank button : + // 0x65: Mirror Chip : 'direction', 'next' + // 0x66: Mirror Melinda : 'direction', 'next' + // 0x67: (Unused) : + // 0x68: Bowling ball : 'next' + // 0x69: Rover : 'direction', 'next' + // 0x6a: Time penalty : 'next' + // 0x6b: Custom floor (green) : Modifier allows other styles, see below + // 0x6c: (Unused) : + // 0x6d: Thin wall / Canopy : Panel/Canopy bitmask (see below), 'next' + // 0x6e: (Unused) : + // 0x6f: Railroad sign : 'next' + // 0x70: Custom wall (green) : Modifier allows other styles, see below + // TODO needs a preceding modifier but that's not done yet (and should enforce that a modifier is followed by a modifiable tile?) + 0x71: 'floor_letter', + // 0x72: Purple toggle wall : + // 0x73: Purple toggle floor : + // 0x74: (Unused) : + // 0x75: (Unused) : + // 0x76: 8-bit Modifier (see Modifier section below) : 1 modifier byte, Tile Specification for affected tile + // 0x77: 16-bit Modifier (see Modifier section below) : 2 modifier bytes, Tile Specification for affected tile + // 0x78: 32-bit Modifier (see Modifier section below) : 4 modifier bytes, Tile Specification for affected tile + // 0x79: (Unused) : 'direction', 'next' + 0x7a: ['score_10', 'next'], + 0x7b: ['score_100', 'next'], + 0x7c: ['score_1000', 'next'], + // 0x7d: Solid green wall : + // 0x7e: False green wall : + 0x7f: ['forbidden', 'next'], + 0x80: ['score_2x', 'next'], + // 0x81: Directional block : 'direction', Directional Arrows Bitmask, 'next' + // 0x82: Floor mimic : 'direction', 'next' + // 0x83: Green bomb : 'next' + // 0x84: Green chip : 'next' + // 0x85: (Unused) : 'next' + // 0x86: (Unused) : 'next' + // 0x87: Black button : + // 0x88: ON/OFF switch (OFF) : + // 0x89: ON/OFF switch (ON) : + 0x8a: 'thief_tools', + // 0x8b: Ghost : 'direction', 'next' + // 0x8c: Steel foil : 'next' + 0x8d: 'turtle', + // 0x8e: Secret eye : 'next' + // 0x8f: Thief bribe : 'next' + // 0x90: Speed boots : 'next' + // 0x91: (Unused) : + // 0x92: Hook : 'next' +}; // Decompress the little ad-hoc compression scheme used for both map data and // solution playback @@ -141,13 +292,27 @@ export function parse_level(buf) { while (true) { let tile_byte = bytes[p]; p++; - let tile_name = CC2_TILE_TYPES[tile_byte]; - if (! tile_name) + if (tile_byte >= 0x76 && tile_byte <= 0x78) { + // XXX handle these modifier "tiles" + p += tile_byte - 0x75; + continue; + } + let spec = TILE_ENCODING[tile_byte]; + if (! spec) throw new Error(`Unrecognized tile type 0x${tile_byte.toString(16)}`); - let tile = {name: tile_name}; + let name; + let args = []; + if (spec instanceof Array) { + [name, ...args] = spec; + } + else { + name = spec; + } + let tile = {name}; cell.push(tile); - let tiledef = TILE_TYPES[tile_name]; + let tiledef = TILE_TYPES[name]; + if (!tiledef) console.error(name); if (tiledef.is_required_chip) { level.chips_required++; } @@ -156,17 +321,26 @@ export function parse_level(buf) { level.player_start_x = n % width; level.player_start_y = Math.floor(n / width); } - if (tiledef.has_direction) { - let dirbyte = bytes[p]; - p++; - let direction = ['north', 'east', 'south', 'west'][dirbyte]; - if (! direction) { - console.warn(`'${tile_name}' tile at ${n % width}, ${Math.floor(n / width)} has bogus direction byte ${dirbyte}; defaulting to south`); - direction = 'south'; + + // Handle extra arguments + let has_next = false; + for (let arg of args) { + if (arg === 'direction') { + let dirbyte = bytes[p]; + p++; + let direction = ['north', 'east', 'south', 'west'][dirbyte]; + if (! direction) { + console.warn(`'${name}' tile at ${n % width}, ${Math.floor(n / width)} has bogus direction byte ${dirbyte}; defaulting to south`); + direction = 'south'; + } + tile.direction = direction; + } + else if (arg === 'next') { + has_next = true; } - tile.direction = direction; } - if (! tiledef.is_top_layer) + + if (! has_next) break; } level.linear_cells.push(cell); diff --git a/js/format-dat.js b/js/format-dat.js index 273ae90..78b7c64 100644 --- a/js/format-dat.js +++ b/js/format-dat.js @@ -1,7 +1,7 @@ import * as util from './format-util.js'; -import { TILE_TYPES, CC2_TILE_TYPES } from './tiletypes.js'; +import TILE_TYPES from './tiletypes.js'; -const CC1_TILE_ENCODING = { +const TILE_ENCODING = { 0x00: 'floor', 0x01: 'wall', 0x02: 'chip', @@ -151,15 +151,18 @@ function parse_level(buf) { p += 2; } - let name = CC1_TILE_ENCODING[tile_byte]; + let spec = TILE_ENCODING[tile_byte]; // TODO could be more forgiving for goofy levels doing goofy things - if (! name) + if (! spec) // TODO doesn't say what level or where in the file, come on throw new Error(`Invalid tile byte: 0x${tile_byte.toString(16)}`); - let direction; - if (name instanceof Array) { - [name, direction] = name; + let name, direction; + if (spec instanceof Array) { + [name, direction] = spec; + } + else { + name = spec; } let tile_type = TILE_TYPES[name]; diff --git a/js/main.js b/js/main.js index 1112cae..2615881 100644 --- a/js/main.js +++ b/js/main.js @@ -3,7 +3,7 @@ import * as c2m from './format-c2m.js'; import * as dat from './format-dat.js'; import * as format_util from './format-util.js'; -import { TILE_TYPES, CC2_TILE_TYPES } from './tiletypes.js'; +import TILE_TYPES from './tiletypes.js'; import { Tileset, CC2_TILESET_LAYOUT, TILE_WORLD_TILESET_LAYOUT } from './tileset.js'; function mk(tag_selector, ...children) { @@ -68,14 +68,11 @@ async function fetch(url) { const PAGE_TITLE = "Lexy's Labyrinth"; class Tile { - constructor(type, x, y, direction = null) { + constructor(type, x, y, direction = 'south') { this.type = type; this.x = x; this.y = y; this.direction = direction; - if (type.has_direction && ! direction) { - this.direction = 'south'; - } this.slide_mode = null; @@ -85,6 +82,7 @@ class Tile { } static from_template(tile_template, x, y) { + if (! TILE_TYPES[tile_template.name]) console.error(tile_template.name); return new this(TILE_TYPES[tile_template.name], x, y, tile_template.direction); } @@ -92,13 +90,15 @@ class Tile { if (this.type.ignores && this.type.ignores.has(name)) return true; - for (let [item, count] of Object.entries(this.inventory)) { - if (count === 0) - continue; + if (this.inventory) { + for (let [item, count] of Object.entries(this.inventory)) { + if (count === 0) + continue; - let item_type = TILE_TYPES[item]; - if (item_type.item_ignores && item_type.item_ignores.has(name)) - return true; + let item_type = TILE_TYPES[item]; + if (item_type.item_ignores && item_type.item_ignores.has(name)) + return true; + } } return false; @@ -179,21 +179,25 @@ const DIRECTIONS = { movement: [0, -1], left: 'west', right: 'east', + opposite: 'south', }, south: { movement: [0, 1], left: 'east', right: 'west', + opposite: 'north', }, west: { movement: [-1, 0], left: 'south', right: 'north', + opposite: 'east', }, east: { movement: [1, 0], left: 'north', right: 'south', + opposite: 'west', }, }; @@ -272,18 +276,20 @@ class Level { } for (let actor of this.actors) { - // TODO skip doomed? strip them out? hm - if (actor.slide_mode === 'ice') { - // Actors can't make voluntary moves on ice + // TODO strip these out maybe?? + if (actor.doomed) + continue; + + // Actors can't make voluntary moves on ice + if (actor.slide_mode === 'ice') continue; - } if (actor === this.player) { if (player_direction) { actor.direction = player_direction; this.attempt_step(actor, player_direction); } } - else { + else if (actor.type.movement_mode === 'follow-left') { // bug behavior: always try turning as left as possible, and // fall back to less-left turns when that fails let direction = DIRECTIONS[actor.direction].left; @@ -295,6 +301,48 @@ class Level { direction = DIRECTIONS[direction].right; } } + else if (actor.type.movement_mode === 'follow-right') { + // paramecium behavior: always try turning as right as + // possible, and fall back to less-right turns when that fails + let direction = DIRECTIONS[actor.direction].right; + for (let i = 0; i < 4; i++) { + if (this.attempt_step(actor, direction)) { + actor.direction = direction; + break; + } + direction = DIRECTIONS[direction].left; + } + } + else if (actor.type.movement_mode === 'turn-left') { + // glider behavior: preserve current direction; if that doesn't + // work, turn left, then right, then back the way we came + for (let direction of [ + actor.direction, + DIRECTIONS[actor.direction].left, + DIRECTIONS[actor.direction].right, + DIRECTIONS[actor.direction].opposite, + ]) { + if (this.attempt_step(actor, direction)) { + actor.direction = direction; + break; + } + } + } + else if (actor.type.movement_mode === 'turn-right') { + // fireball behavior: preserve current direction; if that doesn't + // work, turn right, then left, then back the way we came + for (let direction of [ + actor.direction, + DIRECTIONS[actor.direction].right, + DIRECTIONS[actor.direction].left, + DIRECTIONS[actor.direction].opposite, + ]) { + if (this.attempt_step(actor, direction)) { + actor.direction = direction; + break; + } + } + } // TODO do i need to do this more aggressively? if (this.state === 'success' || this.state === 'failure') @@ -311,31 +359,37 @@ class Level { let move = DIRECTIONS[direction].movement; let goal_x = actor.x + move[0]; let goal_y = actor.y + move[1]; - let goal_cell = this.cells[goal_y][goal_x]; - let blocks; - goal_cell.each(tile => { - if (tile !== actor && tile.type.blocks) { - if (actor.type.pushes && actor.type.pushes[tile.type.name]) { - if (this.attempt_step(tile, direction)) - // It moved out of the way! - return; + let blocked; + if (goal_x >= 0 && goal_x < this.width && goal_y >= 0 && goal_y < this.height) { + let goal_cell = this.cells[goal_y][goal_x]; + goal_cell.each(tile => { + if (tile !== actor && tile.type.blocks) { + if (actor.type.pushes && actor.type.pushes[tile.type.name]) { + if (this.attempt_step(tile, direction)) + // It moved out of the way! + return; + } + if (tile.type.on_bump) { + tile.type.on_bump(tile, this, actor); + if (! tile.type.blocks) + // It became something non-blocking! + return; + } + blocked = true; + // XXX should i break here, or bump everything? } - if (tile.type.on_bump) { - tile.type.on_bump(tile, this, actor); - if (! tile.type.blocks) - // It became something non-blocking! - return; - } - blocks = true; - // XXX should i break here, or bump everything? - } - }); + }); + } + else { + // Hit the edge + blocked = true; + } - if (blocks) { + if (blocked) { if (actor.slide_mode === 'ice') { // Actors on ice turn around when they hit something - actor.direction = DIRECTIONS[DIRECTIONS[direction].left].left; + actor.direction = DIRECTIONS[direction].opposite; } return false; } @@ -419,11 +473,13 @@ class Game { this.tileset = tileset; // TODO obey level options; allow overriding - this.camera_size_x = 9; - this.camera_size_y = 9; + this.viewport_size_x = 19; + this.viewport_size_y = 19; - this.container = document.body; - this.container.innerHTML = GAME_UI_HTML; + document.body.innerHTML = GAME_UI_HTML; + this.container = document.body.querySelector('main'); + this.container.style.setProperty('--tile-width', `${this.tileset.size_x}px`); + this.container.style.setProperty('--tile-height', `${this.tileset.size_y}px`); this.level_el = this.container.querySelector('.level'); this.meta_el = this.container.querySelector('.meta'); this.nav_el = this.container.querySelector('.nav'); @@ -433,6 +489,7 @@ class Game { this.inventory_el = this.container.querySelector('.inventory'); this.bummer_el = this.container.querySelector('.bummer'); + // Populate navigation this.nav_prev_button = this.nav_el.querySelector('.nav-prev'); this.nav_next_button = this.nav_el.querySelector('.nav-next'); this.nav_prev_button.addEventListener('click', ev => { @@ -448,17 +505,29 @@ class Game { } }); - this.load_level(0); + // Populate inventory + this._inventory_tiles = {}; + let floor_tile = this.render_inventory_tile('floor'); + this.inventory_el.style.backgroundImage = `url(${floor_tile})`; - this.level_canvas = mk('canvas', {width: tileset.size_x * this.camera_size_x, height: tileset.size_y * this.camera_size_y}); + this.level_canvas = mk('canvas', {width: tileset.size_x * this.viewport_size_x, height: tileset.size_y * this.viewport_size_y}); this.level_el.append(this.level_canvas); this.level_canvas.setAttribute('tabindex', '-1'); + this.level_canvas.addEventListener('auxclick', ev => { + if (ev.button !== 1) + return; + + let rect = this.level_canvas.getBoundingClientRect(); + let x = Math.floor((ev.clientX - rect.x) / 2 / this.tileset.size_x + this.viewport_x); + let y = Math.floor((ev.clientY - rect.y) / 2 / this.tileset.size_y + this.viewport_y); + this.level.move_to(this.level.player, x, y); + }); let last_key; this.pending_player_move = null; this.next_player_move = null; this.player_used_move = false; - let key_target = this.container; + let key_target = document.body; // TODO this could all probably be more rigorous but it's fine for now key_target.addEventListener('keydown', ev => { let direction; @@ -495,6 +564,8 @@ class Game { } }); + // Done with UI, now we can load a level + this.load_level(0); this.redraw(); this.frame = 0; @@ -537,6 +608,16 @@ class Game { requestAnimationFrame(this.do_frame.bind(this)); } + render_inventory_tile(name) { + if (! this._inventory_tiles[name]) { + // TODO reuse the canvas + let canvas = mk('canvas', {width: this.tileset.size_x, height: this.tileset.size_y}); + this.tileset.draw({type: TILE_TYPES[name]}, canvas.getContext('2d'), 0, 0); + this._inventory_tiles[name] = canvas.toDataURL(); + } + return this._inventory_tiles[name]; + } + update_ui() { // TODO can we do this only if they actually changed? this.chips_el.textContent = this.level.chips_remaining; @@ -548,17 +629,30 @@ class Game { else { this.bummer_el.textContent = ''; } + + this.inventory_el.textContent = ''; + for (let [name, count] of Object.entries(this.level.player.inventory)) { + if (count > 0) { + this.inventory_el.append(mk('img', {src: this.render_inventory_tile(name)})); + } + } } redraw() { let ctx = this.level_canvas.getContext('2d'); ctx.clearRect(0, 0, this.level_canvas.width, this.level_canvas.height); - let camera_x = this.level.player.x - (this.camera_size_x - 1) / 2; - let camera_y = this.level.player.y - (this.camera_size_y - 1) / 2; - for (let dx = 0; dx < this.camera_size_x; dx++) { - for (let dy = 0; dy < this.camera_size_y; dy++) { - let cell = this.level.cells[dy + camera_y][dx + camera_x]; + let xmargin = (this.viewport_size_x - 1) / 2; + let ymargin = (this.viewport_size_y - 1) / 2; + let x0 = this.level.player.x - xmargin; + let y0 = this.level.player.y - ymargin; + x0 = Math.max(0, Math.min(this.level.width - this.viewport_size_x, x0)); + y0 = Math.max(0, Math.min(this.level.height - this.viewport_size_y, y0)); + this.viewport_x = x0; + this.viewport_y = y0; + for (let dx = 0; dx < this.viewport_size_x; dx++) { + for (let dy = 0; dy < this.viewport_size_y; dy++) { + let cell = this.level.cells[dy + y0][dx + x0]; /* if (! cell.is_dirty) continue; @@ -626,6 +720,10 @@ async function main() { stored_game = dat.parse_game(await fetch('levels/CCLP1.ccl')); } let game = new Game(stored_game, tileset); + + if (query.get('debug')) { + game.debug = true; + } } main(); diff --git a/js/tileset.js b/js/tileset.js index f756b82..30c137e 100644 --- a/js/tileset.js +++ b/js/tileset.js @@ -1,27 +1,42 @@ export const CC2_TILESET_LAYOUT = { floor: [0, 2], + floor_letter: [2, 2], wall: [1, 2], - ice: [10, 1], - ice_sw: [12, 1], - ice_nw: [14, 1], - ice_ne: [13, 1], - ice_se: [11, 1], - water: [ - [12, 24], - [13, 24], - [14, 24], - [15, 24], - ], + fire: [ [12, 29], [13, 29], [14, 29], [15, 29], ], + water: [ + [12, 24], + [13, 24], + [14, 24], + [15, 24], + ], + ice: [10, 1], + ice_sw: [12, 1], + ice_nw: [14, 1], + ice_ne: [13, 1], + ice_se: [11, 1], force_floor_n: [[0, 19], [0, 20]], force_floor_e: [[2, 19], [2, 20]], force_floor_s: [[1, 19], [1, 20]], force_floor_w: [[3, 19], [3, 20]], + thief_keys: [15, 21], + thief_tools: [3, 2], + + // TODO these guys don't have floor underneath. + swivel_sw: [9, 11], + swivel_nw: [10, 11], + swivel_ne: [12, 11], + swivel_se: [13, 11], + forbidden: [14, 5], + turtle: [13, 12], // TODO also 14 + 15 for sinking + popwall: [8, 10], + bomb: [5, 4], + exit: [ [6, 2], @@ -72,6 +87,11 @@ export const CC2_TILESET_LAYOUT = { flippers: [0, 6], hint: [5, 2], + + score_10: [14, 1], + score_100: [13, 1], + score_1000: [12, 1], + score_2x: [15, 1], }; export const TILE_WORLD_TILESET_LAYOUT = { @@ -200,6 +220,7 @@ export class Tileset { draw(tile, ctx, x, y) { let drawspec = this.layout[tile.type.name]; let coords = drawspec; + if (! coords) console.error(tile.type.name); if (!(coords instanceof Array)) { // Must be an object of directions coords = coords[tile.direction ?? 'south']; diff --git a/js/tiletypes.js b/js/tiletypes.js index 5769fb2..1abe4af 100644 --- a/js/tiletypes.js +++ b/js/tiletypes.js @@ -1,15 +1,10 @@ -export const TILE_TYPES = { - cloner: { - blocks: true, - }, - - - +const TILE_TYPES = { + // Floors and walls floor: { - cc2_byte: 0x01, + }, + floor_letter: { }, wall: { - cc2_byte: 0x02, blocks: true, }, wall_invisible: { @@ -18,6 +13,8 @@ export const TILE_TYPES = { wall_appearing: { blocks: true, }, + popwall: { + }, thinwall_n: { thin_walls: new Set(['north']), }, @@ -43,78 +40,77 @@ export const TILE_TYPES = { } }, - ice: { - cc2_byte: 0x03, - on_arrive(me, level, other) { - level.make_slide(other, 'ice'); + // Swivel doors + swivel_ne: { + thin_walls: new Set(['north'], ['east']), + }, + swivel_se: { + thin_walls: new Set(['south'], ['east']), + }, + swivel_sw: { + thin_walls: new Set(['south'], ['west']), + }, + swivel_nw: { + thin_walls: new Set(['north'], ['west']), + }, + + // Locked doors + door_red: { + blocks: true, + on_bump(me, level, other) { + if (other.type.has_inventory && other.take_item('key_red')) { + me.type = TILE_TYPES.floor; + } } }, - ice_sw: { - cc2_byte: 0x04, - thin_walls: { - south: true, - west: true, - }, - on_arrive(me, level, other) { - if (other.direction === 'south') { - other.direction = 'east'; + door_blue: { + blocks: true, + on_bump(me, level, other) { + if (other.type.has_inventory && other.take_item('key_blue')) { + me.type = TILE_TYPES.floor; } - else { - other.direction = 'north'; - } - level.make_slide(other, 'ice'); } }, - ice_nw: { - cc2_byte: 0x05, - thin_walls: { - north: true, - west: true, - }, - on_arrive(me, level, other) { - if (other.direction === 'north') { - other.direction = 'east'; + door_yellow: { + blocks: true, + on_bump(me, level, other) { + if (other.type.has_inventory && other.take_item('key_yellow')) { + me.type = TILE_TYPES.floor; } - else { - other.direction = 'south'; - } - level.make_slide(other, 'ice'); } }, - ice_ne: { - cc2_byte: 0x06, - thin_walls: { - north: true, - east: true, - }, - on_arrive(me, level, other) { - if (other.direction === 'north') { - other.direction = 'west'; + door_green: { + blocks: true, + on_bump(me, level, other) { + if (other.type.has_inventory && other.take_item('key_green')) { + me.type = TILE_TYPES.floor; } - else { - other.direction = 'south'; - } - level.make_slide(other, 'ice'); } }, - ice_se: { - cc2_byte: 0x07, - thin_walls: { - south: true, - east: true, - }, + + // Terrain + dirt: { + // TODO block monsters, and melinda only without the hiking boots on_arrive(me, level, other) { - if (other.direction === 'south') { - other.direction = 'west'; + me.become('floor'); + } + }, + gravel: { + }, + + // Hazards + fire: { + on_arrive(me, level, other) { + if (other.type.is_player) { + level.fail("Oops! You can't walk on fire without fire boots!"); + other.become('player_burned'); } else { - other.direction = 'north'; + other.destroy(); } - level.make_slide(other, 'ice'); } }, water: { - cc2_byte: 0x08, on_arrive(me, level, other) { // TODO cc1 allows items under water, i think; water was on the upper layer if (other.type.name == 'dirt_block') { @@ -130,58 +126,226 @@ export const TILE_TYPES = { } } }, - fire: { - cc2_byte: 0x09, + turtle: { + }, + ice: { on_arrive(me, level, other) { - if (other.type.is_player) { - level.fail("Oops! You can't walk on fire without fire boots!"); - other.become('player_burned'); + level.make_slide(other, 'ice'); + } + }, + ice_sw: { + thin_walls: { + south: true, + west: true, + }, + on_arrive(me, level, other) { + if (other.direction === 'south') { + other.direction = 'east'; } else { - other.destroy(); + other.direction = 'north'; } + level.make_slide(other, 'ice'); + } + }, + ice_nw: { + thin_walls: { + north: true, + west: true, + }, + on_arrive(me, level, other) { + if (other.direction === 'north') { + other.direction = 'east'; + } + else { + other.direction = 'south'; + } + level.make_slide(other, 'ice'); + } + }, + ice_ne: { + thin_walls: { + north: true, + east: true, + }, + on_arrive(me, level, other) { + if (other.direction === 'north') { + other.direction = 'west'; + } + else { + other.direction = 'south'; + } + level.make_slide(other, 'ice'); + } + }, + ice_se: { + thin_walls: { + south: true, + east: true, + }, + on_arrive(me, level, other) { + if (other.direction === 'south') { + other.direction = 'west'; + } + else { + other.direction = 'north'; + } + level.make_slide(other, 'ice'); } }, force_floor_n: { - cc2_byte: 0x0a, on_arrive(me, level, other) { other.direction = 'north'; level.make_slide(other, 'push'); } }, force_floor_e: { - cc2_byte: 0x0b, on_arrive(me, level, other) { other.direction = 'east'; level.make_slide(other, 'push'); } }, force_floor_s: { - cc2_byte: 0x0c, on_arrive(me, level, other) { other.direction = 'south'; level.make_slide(other, 'push'); } }, force_floor_w: { - cc2_byte: 0x0d, on_arrive(me, level, other) { other.direction = 'west'; level.make_slide(other, 'push'); } }, - - exit: { - cc2_byte: 0x14, + bomb: { + }, + thief_tools: { + on_arrive(me, level, other) { + if (other.inventory) { + for (let [name, count] of Object.entries(other.inventory)) { + if (count > 0 && TILE_TYPES[name].is_tool) { + other.take_item(name, count); + } + } + } + } + }, + thief_keys: { + on_arrive(me, level, other) { + if (other.inventory) { + for (let [name, count] of Object.entries(other.inventory)) { + if (count > 0 && TILE_TYPES[name].is_key) { + other.take_item(name, count); + } + } + } + } + }, + forbidden: { }, + // Mechanisms + cloner: { + blocks: true, + }, + dirt_block: { + blocks: true, + is_object: true, + }, + + // Critters + bug: { + is_actor: true, + is_object: true, + movement_mode: 'follow-left', + }, + paramecium: { + is_actor: true, + is_object: true, + }, + ball: { + is_actor: true, + is_object: true, + }, + blob: { + is_actor: true, + is_object: true, + }, + teeth: { + is_actor: true, + is_object: true, + }, + fireball: { + is_actor: true, + is_object: true, + movement_mode: 'turn-right', + ignores: new Set(['fire']), + }, + glider: { + is_actor: true, + is_object: true, + movement_mode: 'turn-left', + ignores: new Set(['water']), + }, + + // Keys + key_red: { + is_object: true, + is_item: true, + is_key: true, + }, + key_blue: { + is_object: true, + is_item: true, + is_key: true, + }, + key_yellow: { + is_object: true, + is_item: true, + is_key: true, + }, + key_green: { + is_object: true, + is_item: true, + is_key: true, + }, + // Tools + cleats: { + is_object: true, + is_item: true, + is_tool: true, + item_ignores: new Set(['ice']), + }, + suction_boots: { + is_object: true, + is_item: true, + is_tool: true, + item_ignores: new Set([ + 'force_floor_n', + 'force_floor_s', + 'force_floor_e', + 'force_floor_w', + ]), + }, + fire_boots: { + is_object: true, + is_item: true, + is_tool: true, + item_ignores: new Set(['fire']), + }, + flippers: { + is_object: true, + is_item: true, + is_tool: true, + item_ignores: new Set(['water']), + }, + + // Progression player: { - cc2_byte: 0x16, is_actor: true, is_player: true, has_inventory: true, - has_direction: true, - is_top_layer: true, + is_object: true, pushes: { dirt_block: true, }, @@ -190,77 +354,11 @@ export const TILE_TYPES = { }, }, player_drowned: { - cc2_byte: null, }, player_burned: { - cc2_byte: null, - }, - dirt_block: { - cc2_byte: 0x17, - blocks: true, - has_direction: true, - is_top_layer: true, - }, - - door_red: { - cc2_byte: 0x22, - blocks: true, - on_bump(me, level, other) { - if (other.type.has_inventory && other.take_item('key_red')) { - me.type = TILE_TYPES.floor; - } - } - }, - door_blue: { - cc2_byte: 0x23, - blocks: true, - on_bump(me, level, other) { - if (other.type.has_inventory && other.take_item('key_blue')) { - me.type = TILE_TYPES.floor; - } - } - }, - door_yellow: { - cc2_byte: 0x24, - blocks: true, - on_bump(me, level, other) { - if (other.type.has_inventory && other.take_item('key_yellow')) { - me.type = TILE_TYPES.floor; - } - } - }, - door_green: { - cc2_byte: 0x25, - blocks: true, - on_bump(me, level, other) { - if (other.type.has_inventory && other.take_item('key_green')) { - me.type = TILE_TYPES.floor; - } - } - }, - key_red: { - cc2_byte: 0x26, - is_top_layer: true, - is_item: true, - }, - key_blue: { - cc2_byte: 0x27, - is_top_layer: true, - is_item: true, - }, - key_yellow: { - cc2_byte: 0x28, - is_top_layer: true, - is_item: true, - }, - key_green: { - cc2_byte: 0x29, - is_top_layer: true, - is_item: true, }, chip: { - cc2_byte: 0x2a, - is_top_layer: true, + is_object: true, is_chip: true, is_required_chip: true, on_arrive(me, level, other) { @@ -271,12 +369,25 @@ export const TILE_TYPES = { } }, chip_extra: { - cc2_byte: 0x2b, is_chip: true, - is_top_layer: true, + is_object: true, + }, + score_10: { + is_object: true, + }, + score_100: { + is_object: true, + }, + score_1000: { + is_object: true, + }, + score_2x: { + is_object: true, + }, + + hint: { }, socket: { - cc2_byte: 0x2c, blocks: true, on_bump(me, level, other) { if (other.type.is_player && level.chips_remaining === 0) { @@ -284,98 +395,13 @@ export const TILE_TYPES = { } } }, - - dirt: { - cc2_byte: 0x32, - // TODO block monsters, and melinda only without the hiking boots - on_arrive(me, level, other) { - me.become('floor'); - } - }, - bug: { - cc2_byte: 0x33, - is_actor: true, - has_direction: true, - is_top_layer: true, - }, - paramecium: { - cc2_byte: 0x34, - is_actor: true, - has_direction: true, - is_top_layer: true, - }, - ball: { - cc2_byte: 0x35, - is_actor: true, - has_direction: true, - is_top_layer: true, - }, - blob: { - cc2_byte: 0x36, - is_actor: true, - has_direction: true, - is_top_layer: true, - }, - teeth: { - cc2_byte: 0x37, - is_actor: true, - has_direction: true, - is_top_layer: true, - }, - fireball: { - cc2_byte: 0x38, - is_actor: true, - has_direction: true, - is_top_layer: true, - }, - - cleats: { - cc2_byte: 0x3b, - is_top_layer: true, - is_item: true, - item_ignores: new Set(['ice']), - }, - suction_boots: { - cc2_byte: 0x3c, - is_top_layer: true, - is_item: true, - item_ignores: new Set([ - 'force_floor_n', - 'force_floor_s', - 'force_floor_e', - 'force_floor_w', - ]), - }, - fire_boots: { - cc2_byte: 0x3d, - is_top_layer: true, - is_item: true, - item_ignores: new Set(['fire']), - }, - flippers: { - cc2_byte: 0x3e, - is_top_layer: true, - is_item: true, - item_ignores: new Set(['water']), - }, - - hint: { - cc2_byte: 0x45, + exit: { }, }; - -export const CC2_TILE_TYPES = new Array(256); -CC2_TILE_TYPES.fill(null); -for (let [name, tiledef] of Object.entries(TILE_TYPES)) { - tiledef.name = name; - - if (tiledef.cc2_byte === null || tiledef.cc2_byte === undefined) - continue; - - let existing = CC2_TILE_TYPES[tiledef.cc2_byte]; - if (existing) - throw new Error(`Duplicate CC2 byte: ${tiledef.cc2_byte} is both '${existing}' and '${name}'`); - - CC2_TILE_TYPES[tiledef.cc2_byte] = name; +// Tell them all their own names +for (let [name, type] of Object.entries(TILE_TYPES)) { + type.name = name; } + +export default TILE_TYPES; diff --git a/style.css b/style.css index cbc2c64..66feece 100644 --- a/style.css +++ b/style.css @@ -24,6 +24,12 @@ main { / min-content 12em ; gap: 1em; + + image-rendering: optimizeSpeed; + + --tile-width: 32px; + --tile-height: 32px; + --scale: 2; } .level { @@ -33,9 +39,7 @@ main { } .level canvas { display: block; - width: calc(9 * 32px * 2); - width: calc(9 * 48px * 2); - image-rendering: optimizeSpeed; + width: calc(9 * var(--tile-width) * var(--scale)); } .meta { grid-area: meta; @@ -55,9 +59,13 @@ main { .chips { grid-area: chips; + padding: 0 0.5em; color: yellow; background: black; } +.chips::before { + content: "chips left: "; +} .time { grid-area: time; } @@ -66,6 +74,16 @@ main { } .inventory { grid-area: inventory; + display: flex; + flex-wrap: wrap; + align-items: start; + + background-size: calc(2 * var(--tile-width)) calc(2 * var(--tile-height)); + width: calc(4 * var(--tile-width) * 2); + min-height: calc(2 * var(--tile-height) * 2); +} +.inventory img { + width: calc(2 * var(--tile-width)); } .bummer { grid-area: level;