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',
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;

View File

@ -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;

View File

@ -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 = `
<main>
<div class="level"><!-- level canvas and any overlays go here --></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="chips"></div>
<div class="time"></div>
@ -375,7 +401,8 @@ const GAME_UI_HTML = `
</main>
`;
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();

View File

@ -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 {

View File

@ -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];

View File

@ -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;