Finish CC1 parser; show hints; improve ice

This commit is contained in:
Eevee (Evelyn Woods) 2020-08-28 05:24:25 -06:00
parent 3084ca7b49
commit 0dd190fc5a
6 changed files with 317 additions and 44 deletions

View File

@ -7,13 +7,19 @@ const CC1_TILE_ENCODING = {
0x02: 'chip', 0x02: 'chip',
0x03: 'water', 0x03: 'water',
0x04: 'fire', 0x04: 'fire',
// invis wall 0x05: 'wall_invisible',
// thin walls... 0x06: 'thinwall_n',
0x07: 'thinwall_w',
0x08: 'thinwall_s',
0x09: 'thinwall_e',
0x0a: 'dirt_block', 0x0a: 'dirt_block',
0x0b: 'dirt', 0x0b: 'dirt',
0x0c: 'ice', 0x0c: 'ice',
0x0d: 'force_floor_s', 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', 0x12: 'force_floor_n',
0x13: 'force_floor_e', 0x13: 'force_floor_e',
0x14: 'force_floor_w', 0x14: 'force_floor_w',
@ -22,24 +28,37 @@ const CC1_TILE_ENCODING = {
0x17: 'door_red', 0x17: 'door_red',
0x18: 'door_green', 0x18: 'door_green',
0x19: 'door_yellow', 0x19: 'door_yellow',
0x1a: 'ice_se', 0x1a: 'ice_nw',
0x1b: 'ice_sw', 0x1b: 'ice_ne',
0x1c: 'ice_nw', 0x1c: 'ice_se',
0x1d: 'ice_nw', 0x1d: 'ice_sw',
// fake blocks 0x1e: 'fake_floor',
// 0x20 unused 0x1f: 'fake_wall',
// thief 0x20: 'wall_invisible', // unused
0x21: 'thief_tools',
0x22: 'socket', 0x22: 'socket',
// green button 0x23: 'button_green',
// red button 0x24: 'button_red',
// green tile 0x25: 'green_wall',
// more buttons, teleports, bombs, traps 0x26: 'green_floor',
0x2f: 'clue', 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', 0x33: 'player_drowned',
0x34: 'player_burned', 0x34: 'player_burned',
//0x35: player_burned, XXX is this burned off a tile or? //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, //0x39: exit_player,
0x3a: 'exit', 0x3a: 'exit',
0x3b: 'exit', // i think this is for the second frame of the exit animation? 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'], 0x41: ['bug', 'west'],
0x42: ['bug', 'south'], 0x42: ['bug', 'south'],
0x43: ['bug', 'east'], 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', 0x64: 'key_blue',
0x65: 'key_red', 0x65: 'key_red',
0x66: 'key_green', 0x66: 'key_green',
@ -75,7 +125,6 @@ function parse_level(buf) {
let view = new DataView(buf); let view = new DataView(buf);
let bytes = new Uint8Array(buf); let bytes = new Uint8Array(buf);
console.log(bytes);
// Header // Header
let level_number = view.getUint16(0, true); let level_number = view.getUint16(0, true);
@ -138,11 +187,11 @@ function parse_level(buf) {
let meta_length = view.getUint16(p, true); let meta_length = view.getUint16(p, true);
p += 2; p += 2;
let end = p + meta_length; let end = p + meta_length;
while (p < meta_length) { while (p < end) {
// Common header // Common header
let field_type = view.getUint16(p, true); let field_type = view.getUint8(p, true);
let field_length = view.getUint16(p + 2, true); let field_length = view.getUint8(p + 1, true);
p += 4; p += 2;
if (field_type === 0x01) { if (field_type === 0x01) {
// Level time; unnecessary since it's already in the level header // Level time; unnecessary since it's already in the level header
// TODO check, compare, warn? // TODO check, compare, warn?
@ -213,14 +262,12 @@ export function parse_game(buf) {
// And now, the levels // And now, the levels
let p = 6; let p = 6;
for (let l = 1; l <= level_count; l++) { for (let l = 1; l <= level_count; l++) {
console.log('level', l);
let length = full_view.getUint16(p, true); let length = full_view.getUint16(p, true);
let level_buf = buf.slice(p + 2, p + 2 + length); let level_buf = buf.slice(p + 2, p + 2 + length);
p += 2 + length; p += 2 + length;
let level = parse_level(level_buf); let level = parse_level(level_buf);
game.levels.push(level); game.levels.push(level);
break;
} }
return game; return game;

View File

@ -9,6 +9,7 @@ export class StoredLevel {
constructor() { constructor() {
this.title = ''; this.title = '';
this.password = null; this.password = null;
this.hint = '';
this.chips_required = 0; this.chips_required = 0;
this.time_limit = 0; this.time_limit = 0;
this.viewport_size = 9; this.viewport_size = 9;

View File

@ -64,7 +64,7 @@ class Tile {
this.direction = 'south'; this.direction = 'south';
} }
this.is_sliding = false; this.slide_mode = null;
if (type.has_inventory) { if (type.has_inventory) {
this.inventory = {}; this.inventory = {};
@ -204,6 +204,8 @@ class Level {
this.actors = []; this.actors = [];
this.chips_remaining = this.stored_level.chips_required; this.chips_remaining = this.stored_level.chips_required;
this.hint_shown = null;
let n = 0; let n = 0;
for (let y = 0; y < this.height; y++) { for (let y = 0; y < this.height; y++) {
let row = []; let row = [];
@ -240,7 +242,7 @@ class Level {
} }
for (let actor of this.actors) { 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? // TODO do we stop sliding if we hit something, too?
this.attempt_step(actor, actor.direction); this.attempt_step(actor, actor.direction);
} }
@ -258,6 +260,10 @@ class Level {
for (let actor of this.actors) { for (let actor of this.actors) {
// TODO skip doomed? strip them out? hm // 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 (actor === this.player) {
if (player_direction) { if (player_direction) {
actor.direction = player_direction; actor.direction = player_direction;
@ -310,12 +316,16 @@ class Level {
} }
blocks = true; blocks = true;
// XXX should i break here, or bump everything? // 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; return false;
}
// We're clear! // We're clear!
this.move_to(actor, goal_x, goal_y); this.move_to(actor, goal_x, goal_y);
@ -329,7 +339,7 @@ class Level {
let goal_cell = this.cells[y][x]; let goal_cell = this.cells[y][x];
let original_cell = this.cells[actor.y][actor.x]; let original_cell = this.cells[actor.y][actor.x];
original_cell._remove(actor); original_cell._remove(actor);
actor.is_sliding = false; actor.slide_mode = null;
goal_cell._add(actor); goal_cell._add(actor);
actor.x = x; actor.x = x;
actor.y = y; actor.y = y;
@ -338,11 +348,19 @@ class Level {
goal_cell.is_dirty = true; goal_cell.is_dirty = true;
// Step on all the tiles in the new cell // Step on all the tiles in the new cell
if (actor === this.player) {
this.hint_shown = null;
}
goal_cell.each(tile => { goal_cell.each(tile => {
if (tile === actor) if (tile === actor)
return; return;
if (actor.ignores(tile.type.name)) if (actor.ignores(tile.type.name))
return; 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) { if (tile.type.is_item && actor.type.has_inventory) {
actor.give_item(tile.type.name); actor.give_item(tile.type.name);
tile.destroy(); tile.destroy();
@ -361,12 +379,20 @@ class Level {
// TODO make a set of primitives for actually altering the level that also // TODO make a set of primitives for actually altering the level that also
// record how to undo themselves // record how to undo themselves
make_slide(actor, mode) {
actor.slide_mode = mode;
}
} }
const GAME_UI_HTML = ` const GAME_UI_HTML = `
<main> <main>
<div class="level"><!-- level canvas and any overlays go here --></div> <div class="level"><!-- level canvas and any overlays go here --></div>
<div class="meta"></div> <div class="meta"></div>
<div class="nav">
<button class="nav-prev" type="button">«</button>
<button class="nav-browse" type="button">Choose level...</button>
<button class="nav-next" type="button">»</button>
</div>
<div class="hint"></div> <div class="hint"></div>
<div class="chips"></div> <div class="chips"></div>
<div class="time"></div> <div class="time"></div>
@ -375,7 +401,8 @@ const GAME_UI_HTML = `
</main> </main>
`; `;
class Game { class Game {
constructor(tileset, level) { constructor(stored_game, tileset) {
this.stored_game = stored_game;
this.tileset = tileset; this.tileset = tileset;
// TODO obey level options; allow overriding // TODO obey level options; allow overriding
@ -386,13 +413,29 @@ class Game {
this.container.innerHTML = GAME_UI_HTML; this.container.innerHTML = GAME_UI_HTML;
this.level_el = this.container.querySelector('.level'); this.level_el = this.container.querySelector('.level');
this.meta_el = this.container.querySelector('.meta'); this.meta_el = this.container.querySelector('.meta');
this.nav_el = this.container.querySelector('.nav');
this.hint_el = this.container.querySelector('.hint'); this.hint_el = this.container.querySelector('.hint');
this.chips_el = this.container.querySelector('.chips'); this.chips_el = this.container.querySelector('.chips');
this.time_el = this.container.querySelector('.time'); this.time_el = this.container.querySelector('.time');
this.inventory_el = this.container.querySelector('.inventory'); this.inventory_el = this.container.querySelector('.inventory');
this.bummer_el = this.container.querySelector('.bummer'); 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_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); this.level_el.append(this.level_canvas);
@ -446,10 +489,14 @@ class Game {
requestAnimationFrame(this.do_frame.bind(this)); requestAnimationFrame(this.do_frame.bind(this));
} }
load_level(level) { load_level(level_index) {
this.level = level; this.level_index = level_index;
this.level = new Level(this.stored_game.levels[level_index]);
// FIXME do better // FIXME do better
this.meta_el.textContent = this.level.stored_level.title; 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(); this.update_ui();
} }
@ -476,7 +523,9 @@ class Game {
} }
update_ui() { update_ui() {
// TODO can we do this only if they actually changed?
this.chips_el.textContent = this.level.chips_remaining; this.chips_el.textContent = this.level.chips_remaining;
this.hint_el.textContent = this.level.hint_shown ?? '';
if (this.level.state === 'failure') { if (this.level.state === 'failure') {
this.bummer_el.textContent = this.level.fail_message; 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 also support tile world's DAC when reading from local??
// TODO ah, there's more metadata in CCX, crapola // TODO ah, there's more metadata in CCX, crapola
let stored_game = await load_game('levels/CCLP1.ccl'); let stored_game = await load_game('levels/CCLP1.ccl');
let level = new Level(stored_game.levels[0]); let game = new Game(stored_game, tileset);
let game = new Game(tileset, level);
} }
main(); main();

View File

@ -62,18 +62,27 @@ export const CC2_TILESET_LAYOUT = {
west: [[12, 7], [13, 7], [14, 7], [15, 7]], 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], cleats: [2, 6],
suction_boots: [3, 6], suction_boots: [3, 6],
fire_boots: [1, 6], fire_boots: [1, 6],
flippers: [0, 6], flippers: [0, 6],
clue: [5, 2], hint: [5, 2],
}; };
export const TILE_WORLD_TILESET_LAYOUT = { export const TILE_WORLD_TILESET_LAYOUT = {
floor: [0, 0], floor: [0, 0],
wall: [0, 1], wall: [0, 1],
thinwall_n: [0, 6],
thinwall_w: [0, 7],
thinwall_s: [0, 8],
thinwall_e: [0, 9],
ice: [0, 12], ice: [0, 12],
ice_sw: [1, 13], ice_sw: [1, 13],
ice_nw: [1, 10], ice_nw: [1, 10],
ice_ne: [1, 11], ice_ne: [1, 11],
@ -84,6 +93,9 @@ export const TILE_WORLD_TILESET_LAYOUT = {
force_floor_e: [1, 3], force_floor_e: [1, 3],
force_floor_s: [0, 13], force_floor_s: [0, 13],
force_floor_w: [1, 4], 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]], exit: [[3, 10], [3, 11]],
@ -93,6 +105,7 @@ export const TILE_WORLD_TILESET_LAYOUT = {
west: [6, 13], west: [6, 13],
east: [6, 15], east: [6, 15],
}, },
cloner: [3, 1],
player_drowned: [3, 3], player_drowned: [3, 3],
player_burned: [3, 4], player_burned: [3, 4],
// TODO the tileset has several of these...? why? // TODO the tileset has several of these...? why?
@ -118,13 +131,62 @@ export const TILE_WORLD_TILESET_LAYOUT = {
south: [4, 2], south: [4, 2],
west: [4, 1], 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], cleats: [6, 10],
suction_boots: [6, 11], suction_boots: [6, 11],
fire_boots: [6, 9], fire_boots: [6, 9],
flippers: [6, 8], flippers: [6, 8],
clue: [2, 15], hint: [2, 15],
}; };
export class Tileset { export class Tileset {

View File

@ -1,4 +1,10 @@
export const TILE_TYPES = { export const TILE_TYPES = {
cloner: {
blocks: true,
},
floor: { floor: {
cc2_byte: 0x01, cc2_byte: 0x01,
}, },
@ -6,8 +12,42 @@ export const TILE_TYPES = {
cc2_byte: 0x02, cc2_byte: 0x02,
blocks: true, 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: { ice: {
cc2_byte: 0x03, cc2_byte: 0x03,
on_arrive(me, level, other) {
level.make_slide(other, 'ice');
}
}, },
ice_sw: { ice_sw: {
cc2_byte: 0x04, cc2_byte: 0x04,
@ -15,6 +55,15 @@ export const TILE_TYPES = {
south: true, south: true,
west: 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: { ice_nw: {
cc2_byte: 0x05, cc2_byte: 0x05,
@ -22,6 +71,15 @@ export const TILE_TYPES = {
north: true, north: true,
west: 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: { ice_ne: {
cc2_byte: 0x06, cc2_byte: 0x06,
@ -29,6 +87,15 @@ export const TILE_TYPES = {
north: true, north: true,
east: 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: { ice_se: {
cc2_byte: 0x07, cc2_byte: 0x07,
@ -36,6 +103,15 @@ export const TILE_TYPES = {
south: true, south: true,
east: 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: { water: {
cc2_byte: 0x08, cc2_byte: 0x08,
@ -70,28 +146,28 @@ export const TILE_TYPES = {
cc2_byte: 0x0a, cc2_byte: 0x0a,
on_arrive(me, level, other) { on_arrive(me, level, other) {
other.direction = 'north'; other.direction = 'north';
other.is_sliding = true; level.make_slide(other, 'push');
} }
}, },
force_floor_e: { force_floor_e: {
cc2_byte: 0x0b, cc2_byte: 0x0b,
on_arrive(me, level, other) { on_arrive(me, level, other) {
other.direction = 'east'; other.direction = 'east';
other.is_sliding = true; level.make_slide(other, 'push');
} }
}, },
force_floor_s: { force_floor_s: {
cc2_byte: 0x0c, cc2_byte: 0x0c,
on_arrive(me, level, other) { on_arrive(me, level, other) {
other.direction = 'south'; other.direction = 'south';
other.is_sliding = true; level.make_slide(other, 'push');
} }
}, },
force_floor_w: { force_floor_w: {
cc2_byte: 0x0d, cc2_byte: 0x0d,
on_arrive(me, level, other) { on_arrive(me, level, other) {
other.direction = 'west'; other.direction = 'west';
other.is_sliding = true; level.make_slide(other, 'push');
} }
}, },
@ -222,6 +298,36 @@ export const TILE_TYPES = {
has_direction: true, has_direction: true,
is_top_layer: 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: { cleats: {
cc2_byte: 0x3b, cc2_byte: 0x3b,
@ -253,7 +359,7 @@ export const TILE_TYPES = {
item_ignores: new Set(['water']), item_ignores: new Set(['water']),
}, },
clue: { hint: {
cc2_byte: 0x45, cc2_byte: 0x45,
}, },
}; };
@ -264,7 +370,7 @@ CC2_TILE_TYPES.fill(null);
for (let [name, tiledef] of Object.entries(TILE_TYPES)) { for (let [name, tiledef] of Object.entries(TILE_TYPES)) {
tiledef.name = name; tiledef.name = name;
if (tiledef.cc2_byte === null) if (tiledef.cc2_byte === null || tiledef.cc2_byte === undefined)
continue; continue;
let existing = CC2_TILE_TYPES[tiledef.cc2_byte]; let existing = CC2_TILE_TYPES[tiledef.cc2_byte];

View File

@ -16,6 +16,7 @@ main {
display: grid; display: grid;
grid: grid:
"level meta" min-content "level meta" min-content
"level nav" min-content
"level chips" min-content "level chips" min-content
"level time" min-content "level time" min-content
"level hint" 1fr "level hint" 1fr
@ -43,6 +44,14 @@ main {
background: black; background: black;
text-align: center; text-align: center;
} }
.nav {
grid-area: nav;
display: flex;
gap: 1em;
}
.nav .nav-browse {
flex: 1;
}
.chips { .chips {
grid-area: chips; grid-area: chips;