Split out CC2 tile bytes; stub out enough for Lesson 1 to load; show inventory; implement misc bits

This commit is contained in:
Eevee (Evelyn Woods) 2020-08-28 07:01:28 -06:00
parent 15d9101ebf
commit bbfa0a6e8f
6 changed files with 663 additions and 323 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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