diff --git a/js/format-dat.js b/js/format-dat.js index e2301a7..273ae90 100644 --- a/js/format-dat.js +++ b/js/format-dat.js @@ -7,13 +7,19 @@ const CC1_TILE_ENCODING = { 0x02: 'chip', 0x03: 'water', 0x04: 'fire', - // invis wall - // thin walls... + 0x05: 'wall_invisible', + 0x06: 'thinwall_n', + 0x07: 'thinwall_w', + 0x08: 'thinwall_s', + 0x09: 'thinwall_e', 0x0a: 'dirt_block', 0x0b: 'dirt', 0x0c: 'ice', 0x0d: 'force_floor_s', - // cloners + 0x0e: ['clone_block', 'north'], + 0x0f: ['clone_block', 'west'], + 0x10: ['clone_block', 'south'], + 0x11: ['clone_block', 'east'], 0x12: 'force_floor_n', 0x13: 'force_floor_e', 0x14: 'force_floor_w', @@ -22,24 +28,37 @@ const CC1_TILE_ENCODING = { 0x17: 'door_red', 0x18: 'door_green', 0x19: 'door_yellow', - 0x1a: 'ice_se', - 0x1b: 'ice_sw', - 0x1c: 'ice_nw', - 0x1d: 'ice_nw', - // fake blocks - // 0x20 unused - // thief + 0x1a: 'ice_nw', + 0x1b: 'ice_ne', + 0x1c: 'ice_se', + 0x1d: 'ice_sw', + 0x1e: 'fake_floor', + 0x1f: 'fake_wall', + 0x20: 'wall_invisible', // unused + 0x21: 'thief_tools', 0x22: 'socket', - // green button - // red button - // green tile - // more buttons, teleports, bombs, traps - 0x2f: 'clue', - + 0x23: 'button_green', + 0x24: 'button_red', + 0x25: 'green_wall', + 0x26: 'green_floor', + 0x27: 'button_brown', + 0x28: 'button_blue', + 0x29: 'teleport_blue', + 0x2a: 'bomb', + 0x2b: 'trap', + 0x2c: 'wall_appearing', + 0x2d: 'gravel', + 0x2e: 'popwall', + 0x2f: 'hint', + 0x30: 'thinwall_se', + 0x31: 'cloner', + 0x32: 'force_floor_all', 0x33: 'player_drowned', 0x34: 'player_burned', //0x35: player_burned, XXX is this burned off a tile or? - // 0x36 - 0x38 unused + 0x36: 'wall_invisible', // unused + 0x37: 'wall_invisible', // unused + 0x38: 'wall_invisible', // unused //0x39: exit_player, 0x3a: 'exit', 0x3b: 'exit', // i think this is for the second frame of the exit animation? @@ -48,7 +67,38 @@ const CC1_TILE_ENCODING = { 0x41: ['bug', 'west'], 0x42: ['bug', 'south'], 0x43: ['bug', 'east'], - + 0x44: ['fireball', 'north'], + 0x45: ['fireball', 'west'], + 0x46: ['fireball', 'south'], + 0x47: ['fireball', 'east'], + 0x48: ['ball', 'north'], + 0x49: ['ball', 'west'], + 0x4a: ['ball', 'south'], + 0x4b: ['ball', 'east'], + 0x4c: ['tank_blue', 'north'], + 0x4d: ['tank_blue', 'west'], + 0x4e: ['tank_blue', 'south'], + 0x4f: ['tank_blue', 'east'], + 0x50: ['glider', 'north'], + 0x51: ['glider', 'west'], + 0x52: ['glider', 'south'], + 0x53: ['glider', 'east'], + 0x54: ['teeth', 'north'], + 0x55: ['teeth', 'west'], + 0x56: ['teeth', 'south'], + 0x57: ['teeth', 'east'], + 0x58: ['walker', 'north'], + 0x59: ['walker', 'west'], + 0x5a: ['walker', 'south'], + 0x5b: ['walker', 'east'], + 0x5c: ['blob', 'north'], + 0x5d: ['blob', 'west'], + 0x5e: ['blob', 'south'], + 0x5f: ['blob', 'east'], + 0x60: ['paramecium', 'north'], + 0x61: ['paramecium', 'west'], + 0x62: ['paramecium', 'south'], + 0x63: ['paramecium', 'east'], 0x64: 'key_blue', 0x65: 'key_red', 0x66: 'key_green', @@ -75,7 +125,6 @@ function parse_level(buf) { let view = new DataView(buf); let bytes = new Uint8Array(buf); - console.log(bytes); // Header let level_number = view.getUint16(0, true); @@ -138,11 +187,11 @@ function parse_level(buf) { let meta_length = view.getUint16(p, true); p += 2; let end = p + meta_length; - while (p < meta_length) { + while (p < end) { // Common header - let field_type = view.getUint16(p, true); - let field_length = view.getUint16(p + 2, true); - p += 4; + let field_type = view.getUint8(p, true); + let field_length = view.getUint8(p + 1, true); + p += 2; if (field_type === 0x01) { // Level time; unnecessary since it's already in the level header // TODO check, compare, warn? @@ -213,14 +262,12 @@ export function parse_game(buf) { // And now, the levels let p = 6; for (let l = 1; l <= level_count; l++) { - console.log('level', l); let length = full_view.getUint16(p, true); let level_buf = buf.slice(p + 2, p + 2 + length); p += 2 + length; let level = parse_level(level_buf); game.levels.push(level); - break; } return game; diff --git a/js/format-util.js b/js/format-util.js index 71c39f5..3211ab0 100644 --- a/js/format-util.js +++ b/js/format-util.js @@ -9,6 +9,7 @@ export class StoredLevel { constructor() { this.title = ''; this.password = null; + this.hint = ''; this.chips_required = 0; this.time_limit = 0; this.viewport_size = 9; diff --git a/js/main.js b/js/main.js index afc4cbb..befdb0e 100644 --- a/js/main.js +++ b/js/main.js @@ -64,7 +64,7 @@ class Tile { this.direction = 'south'; } - this.is_sliding = false; + this.slide_mode = null; if (type.has_inventory) { this.inventory = {}; @@ -204,6 +204,8 @@ class Level { this.actors = []; this.chips_remaining = this.stored_level.chips_required; + this.hint_shown = null; + let n = 0; for (let y = 0; y < this.height; y++) { let row = []; @@ -240,7 +242,7 @@ class Level { } for (let actor of this.actors) { - if (actor.is_sliding) { + if (actor.slide_mode !== null) { // TODO do we stop sliding if we hit something, too? this.attempt_step(actor, actor.direction); } @@ -258,6 +260,10 @@ 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 + continue; + } if (actor === this.player) { if (player_direction) { actor.direction = player_direction; @@ -310,12 +316,16 @@ class Level { } blocks = true; // XXX should i break here, or bump everything? - return false; } }); - if (blocks) + if (blocks) { + if (actor.slide_mode === 'ice') { + // Actors on ice turn around when they hit something + actor.direction = DIRECTIONS[DIRECTIONS[direction].left].left; + } return false; + } // We're clear! this.move_to(actor, goal_x, goal_y); @@ -329,7 +339,7 @@ class Level { let goal_cell = this.cells[y][x]; let original_cell = this.cells[actor.y][actor.x]; original_cell._remove(actor); - actor.is_sliding = false; + actor.slide_mode = null; goal_cell._add(actor); actor.x = x; actor.y = y; @@ -338,11 +348,19 @@ class Level { goal_cell.is_dirty = true; // Step on all the tiles in the new cell + if (actor === this.player) { + this.hint_shown = null; + } goal_cell.each(tile => { if (tile === actor) return; if (actor.ignores(tile.type.name)) return; + + if (actor === this.player && tile.type.name === 'hint') { + this.hint_shown = this.stored_level.hint; + } + if (tile.type.is_item && actor.type.has_inventory) { actor.give_item(tile.type.name); tile.destroy(); @@ -361,12 +379,20 @@ class Level { // TODO make a set of primitives for actually altering the level that also // record how to undo themselves + make_slide(actor, mode) { + actor.slide_mode = mode; + } } const GAME_UI_HTML = `
+
@@ -375,7 +401,8 @@ const GAME_UI_HTML = `
`; class Game { - constructor(tileset, level) { + constructor(stored_game, tileset) { + this.stored_game = stored_game; this.tileset = tileset; // TODO obey level options; allow overriding @@ -386,13 +413,29 @@ class Game { this.container.innerHTML = GAME_UI_HTML; this.level_el = this.container.querySelector('.level'); this.meta_el = this.container.querySelector('.meta'); + this.nav_el = this.container.querySelector('.nav'); this.hint_el = this.container.querySelector('.hint'); this.chips_el = this.container.querySelector('.chips'); this.time_el = this.container.querySelector('.time'); this.inventory_el = this.container.querySelector('.inventory'); this.bummer_el = this.container.querySelector('.bummer'); - this.load_level(level); + 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 => { + // TODO confirm + if (this.level_index > 0) { + this.load_level(this.level_index - 1); + } + }); + this.nav_next_button.addEventListener('click', ev => { + // TODO confirm + if (this.level_index < this.stored_game.levels.length - 1) { + this.load_level(this.level_index + 1); + } + }); + + this.load_level(0); this.level_canvas = mk('canvas', {width: tileset.size_x * this.camera_size_x, height: tileset.size_y * this.camera_size_y}); this.level_el.append(this.level_canvas); @@ -446,10 +489,14 @@ class Game { requestAnimationFrame(this.do_frame.bind(this)); } - load_level(level) { - this.level = level; + load_level(level_index) { + this.level_index = level_index; + this.level = new Level(this.stored_game.levels[level_index]); // FIXME do better this.meta_el.textContent = this.level.stored_level.title; + + this.nav_prev_button.disabled = level_index <= 0; + this.nav_next_button.disabled = level_index >= this.stored_game.levels.length; this.update_ui(); } @@ -476,7 +523,9 @@ class Game { } update_ui() { + // TODO can we do this only if they actually changed? this.chips_el.textContent = this.level.chips_remaining; + this.hint_el.textContent = this.level.hint_shown ?? ''; if (this.level.state === 'failure') { this.bummer_el.textContent = this.level.fail_message; @@ -555,8 +604,7 @@ async function main() { // TODO also support tile world's DAC when reading from local?? // TODO ah, there's more metadata in CCX, crapola let stored_game = await load_game('levels/CCLP1.ccl'); - let level = new Level(stored_game.levels[0]); - let game = new Game(tileset, level); + let game = new Game(stored_game, tileset); } main(); diff --git a/js/tileset.js b/js/tileset.js index 721b719..f756b82 100644 --- a/js/tileset.js +++ b/js/tileset.js @@ -62,18 +62,27 @@ export const CC2_TILESET_LAYOUT = { west: [[12, 7], [13, 7], [14, 7], [15, 7]], }, + ball: [[10, 10], [11, 10], [12, 10], [13, 10], [14, 10]], + + fireball: [[12, 9], [13, 9], [14, 9], [15, 9]], + cleats: [2, 6], suction_boots: [3, 6], fire_boots: [1, 6], flippers: [0, 6], - clue: [5, 2], + hint: [5, 2], }; export const TILE_WORLD_TILESET_LAYOUT = { floor: [0, 0], wall: [0, 1], + thinwall_n: [0, 6], + thinwall_w: [0, 7], + thinwall_s: [0, 8], + thinwall_e: [0, 9], ice: [0, 12], + ice_sw: [1, 13], ice_nw: [1, 10], ice_ne: [1, 11], @@ -84,6 +93,9 @@ export const TILE_WORLD_TILESET_LAYOUT = { force_floor_e: [1, 3], force_floor_s: [0, 13], force_floor_w: [1, 4], + // TODO there are two of these, which seems self-defeating?? + fake_wall: [1, 14], + fake_floor: [1, 15], exit: [[3, 10], [3, 11]], @@ -93,6 +105,7 @@ export const TILE_WORLD_TILESET_LAYOUT = { west: [6, 13], east: [6, 15], }, + cloner: [3, 1], player_drowned: [3, 3], player_burned: [3, 4], // TODO the tileset has several of these...? why? @@ -118,13 +131,62 @@ export const TILE_WORLD_TILESET_LAYOUT = { south: [4, 2], west: [4, 1], }, + fireball: { + north: [4, 4], + east: [4, 7], + south: [4, 6], + west: [4, 5], + }, + ball: { + north: [4, 8], + east: [4, 11], + south: [4, 10], + west: [4, 9], + }, + tank_blue: { + north: [4, 12], + east: [4, 15], + south: [4, 14], + west: [4, 5], + }, + glider: { + north: [5, 0], + east: [5, 3], + south: [5, 2], + west: [5, 1], + }, + teeth: { + north: [5, 4], + east: [5, 7], + south: [5, 6], + west: [5, 5], + }, + walker: { + north: [5, 8], + east: [5, 11], + south: [5, 10], + west: [5, 9], + }, + blob: { + north: [5, 12], + east: [5, 15], + south: [5, 14], + west: [5, 5], + }, + + paramecium: { + north: [6, 0], + east: [6, 3], + south: [6, 2], + west: [6, 1], + }, cleats: [6, 10], suction_boots: [6, 11], fire_boots: [6, 9], flippers: [6, 8], - clue: [2, 15], + hint: [2, 15], }; export class Tileset { diff --git a/js/tiletypes.js b/js/tiletypes.js index 278c442..5769fb2 100644 --- a/js/tiletypes.js +++ b/js/tiletypes.js @@ -1,4 +1,10 @@ export const TILE_TYPES = { + cloner: { + blocks: true, + }, + + + floor: { cc2_byte: 0x01, }, @@ -6,8 +12,42 @@ export const TILE_TYPES = { cc2_byte: 0x02, blocks: true, }, + wall_invisible: { + blocks: true, + }, + wall_appearing: { + blocks: true, + }, + thinwall_n: { + thin_walls: new Set(['north']), + }, + thinwall_s: { + thin_walls: new Set(['south']), + }, + thinwall_e: { + thin_walls: new Set(['east']), + }, + thinwall_w: { + thin_walls: new Set(['west']), + }, + fake_wall: { + blocks: true, + on_bump(me, level, other) { + me.become('wall'); + } + }, + fake_floor: { + blocks: true, + on_bump(me, level, other) { + me.become('floor'); + } + }, + ice: { cc2_byte: 0x03, + on_arrive(me, level, other) { + level.make_slide(other, 'ice'); + } }, ice_sw: { cc2_byte: 0x04, @@ -15,6 +55,15 @@ export const TILE_TYPES = { south: true, west: true, }, + on_arrive(me, level, other) { + if (other.direction === 'south') { + other.direction = 'east'; + } + else { + other.direction = 'north'; + } + level.make_slide(other, 'ice'); + } }, ice_nw: { cc2_byte: 0x05, @@ -22,6 +71,15 @@ export const TILE_TYPES = { 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: { cc2_byte: 0x06, @@ -29,6 +87,15 @@ export const TILE_TYPES = { 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: { cc2_byte: 0x07, @@ -36,6 +103,15 @@ export const TILE_TYPES = { 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'); + } }, water: { cc2_byte: 0x08, @@ -70,28 +146,28 @@ export const TILE_TYPES = { cc2_byte: 0x0a, on_arrive(me, level, other) { other.direction = 'north'; - other.is_sliding = true; + level.make_slide(other, 'push'); } }, force_floor_e: { cc2_byte: 0x0b, on_arrive(me, level, other) { other.direction = 'east'; - other.is_sliding = true; + level.make_slide(other, 'push'); } }, force_floor_s: { cc2_byte: 0x0c, on_arrive(me, level, other) { other.direction = 'south'; - other.is_sliding = true; + level.make_slide(other, 'push'); } }, force_floor_w: { cc2_byte: 0x0d, on_arrive(me, level, other) { other.direction = 'west'; - other.is_sliding = true; + level.make_slide(other, 'push'); } }, @@ -222,6 +298,36 @@ export const TILE_TYPES = { 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, @@ -253,7 +359,7 @@ export const TILE_TYPES = { item_ignores: new Set(['water']), }, - clue: { + hint: { cc2_byte: 0x45, }, }; @@ -264,7 +370,7 @@ CC2_TILE_TYPES.fill(null); for (let [name, tiledef] of Object.entries(TILE_TYPES)) { tiledef.name = name; - if (tiledef.cc2_byte === null) + if (tiledef.cc2_byte === null || tiledef.cc2_byte === undefined) continue; let existing = CC2_TILE_TYPES[tiledef.cc2_byte]; diff --git a/style.css b/style.css index b79f906..cbc2c64 100644 --- a/style.css +++ b/style.css @@ -16,6 +16,7 @@ main { display: grid; grid: "level meta" min-content + "level nav" min-content "level chips" min-content "level time" min-content "level hint" 1fr @@ -43,6 +44,14 @@ main { background: black; text-align: center; } +.nav { + grid-area: nav; + display: flex; + gap: 1em; +} +.nav .nav-browse { + flex: 1; +} .chips { grid-area: chips;