Split out CC2 tile bytes; stub out enough for Lesson 1 to load; show inventory; implement misc bits
This commit is contained in:
parent
15d9101ebf
commit
bbfa0a6e8f
202
js/format-c2m.js
202
js/format-c2m.js
@ -1,5 +1,156 @@
|
|||||||
import * as util from './format-util.js';
|
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
|
// Decompress the little ad-hoc compression scheme used for both map data and
|
||||||
// solution playback
|
// solution playback
|
||||||
@ -141,13 +292,27 @@ export function parse_level(buf) {
|
|||||||
while (true) {
|
while (true) {
|
||||||
let tile_byte = bytes[p];
|
let tile_byte = bytes[p];
|
||||||
p++;
|
p++;
|
||||||
let tile_name = CC2_TILE_TYPES[tile_byte];
|
if (tile_byte >= 0x76 && tile_byte <= 0x78) {
|
||||||
if (! tile_name)
|
// 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)}`);
|
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);
|
cell.push(tile);
|
||||||
let tiledef = TILE_TYPES[tile_name];
|
let tiledef = TILE_TYPES[name];
|
||||||
|
if (!tiledef) console.error(name);
|
||||||
if (tiledef.is_required_chip) {
|
if (tiledef.is_required_chip) {
|
||||||
level.chips_required++;
|
level.chips_required++;
|
||||||
}
|
}
|
||||||
@ -156,17 +321,26 @@ export function parse_level(buf) {
|
|||||||
level.player_start_x = n % width;
|
level.player_start_x = n % width;
|
||||||
level.player_start_y = Math.floor(n / width);
|
level.player_start_y = Math.floor(n / width);
|
||||||
}
|
}
|
||||||
if (tiledef.has_direction) {
|
|
||||||
let dirbyte = bytes[p];
|
// Handle extra arguments
|
||||||
p++;
|
let has_next = false;
|
||||||
let direction = ['north', 'east', 'south', 'west'][dirbyte];
|
for (let arg of args) {
|
||||||
if (! direction) {
|
if (arg === 'direction') {
|
||||||
console.warn(`'${tile_name}' tile at ${n % width}, ${Math.floor(n / width)} has bogus direction byte ${dirbyte}; defaulting to south`);
|
let dirbyte = bytes[p];
|
||||||
direction = 'south';
|
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;
|
break;
|
||||||
}
|
}
|
||||||
level.linear_cells.push(cell);
|
level.linear_cells.push(cell);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import * as util from './format-util.js';
|
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',
|
0x00: 'floor',
|
||||||
0x01: 'wall',
|
0x01: 'wall',
|
||||||
0x02: 'chip',
|
0x02: 'chip',
|
||||||
@ -151,15 +151,18 @@ function parse_level(buf) {
|
|||||||
p += 2;
|
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
|
// 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
|
// TODO doesn't say what level or where in the file, come on
|
||||||
throw new Error(`Invalid tile byte: 0x${tile_byte.toString(16)}`);
|
throw new Error(`Invalid tile byte: 0x${tile_byte.toString(16)}`);
|
||||||
|
|
||||||
let direction;
|
let name, direction;
|
||||||
if (name instanceof Array) {
|
if (spec instanceof Array) {
|
||||||
[name, direction] = name;
|
[name, direction] = spec;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
name = spec;
|
||||||
}
|
}
|
||||||
let tile_type = TILE_TYPES[name];
|
let tile_type = TILE_TYPES[name];
|
||||||
|
|
||||||
|
|||||||
194
js/main.js
194
js/main.js
@ -3,7 +3,7 @@
|
|||||||
import * as c2m from './format-c2m.js';
|
import * as c2m from './format-c2m.js';
|
||||||
import * as dat from './format-dat.js';
|
import * as dat from './format-dat.js';
|
||||||
import * as format_util from './format-util.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';
|
import { Tileset, CC2_TILESET_LAYOUT, TILE_WORLD_TILESET_LAYOUT } from './tileset.js';
|
||||||
|
|
||||||
function mk(tag_selector, ...children) {
|
function mk(tag_selector, ...children) {
|
||||||
@ -68,14 +68,11 @@ async function fetch(url) {
|
|||||||
const PAGE_TITLE = "Lexy's Labyrinth";
|
const PAGE_TITLE = "Lexy's Labyrinth";
|
||||||
|
|
||||||
class Tile {
|
class Tile {
|
||||||
constructor(type, x, y, direction = null) {
|
constructor(type, x, y, direction = 'south') {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.x = x;
|
this.x = x;
|
||||||
this.y = y;
|
this.y = y;
|
||||||
this.direction = direction;
|
this.direction = direction;
|
||||||
if (type.has_direction && ! direction) {
|
|
||||||
this.direction = 'south';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.slide_mode = null;
|
this.slide_mode = null;
|
||||||
|
|
||||||
@ -85,6 +82,7 @@ class Tile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static from_template(tile_template, x, y) {
|
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);
|
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))
|
if (this.type.ignores && this.type.ignores.has(name))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
for (let [item, count] of Object.entries(this.inventory)) {
|
if (this.inventory) {
|
||||||
if (count === 0)
|
for (let [item, count] of Object.entries(this.inventory)) {
|
||||||
continue;
|
if (count === 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
let item_type = TILE_TYPES[item];
|
let item_type = TILE_TYPES[item];
|
||||||
if (item_type.item_ignores && item_type.item_ignores.has(name))
|
if (item_type.item_ignores && item_type.item_ignores.has(name))
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -179,21 +179,25 @@ const DIRECTIONS = {
|
|||||||
movement: [0, -1],
|
movement: [0, -1],
|
||||||
left: 'west',
|
left: 'west',
|
||||||
right: 'east',
|
right: 'east',
|
||||||
|
opposite: 'south',
|
||||||
},
|
},
|
||||||
south: {
|
south: {
|
||||||
movement: [0, 1],
|
movement: [0, 1],
|
||||||
left: 'east',
|
left: 'east',
|
||||||
right: 'west',
|
right: 'west',
|
||||||
|
opposite: 'north',
|
||||||
},
|
},
|
||||||
west: {
|
west: {
|
||||||
movement: [-1, 0],
|
movement: [-1, 0],
|
||||||
left: 'south',
|
left: 'south',
|
||||||
right: 'north',
|
right: 'north',
|
||||||
|
opposite: 'east',
|
||||||
},
|
},
|
||||||
east: {
|
east: {
|
||||||
movement: [1, 0],
|
movement: [1, 0],
|
||||||
left: 'north',
|
left: 'north',
|
||||||
right: 'south',
|
right: 'south',
|
||||||
|
opposite: 'west',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -272,18 +276,20 @@ class Level {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let actor of this.actors) {
|
for (let actor of this.actors) {
|
||||||
// TODO skip doomed? strip them out? hm
|
// TODO strip these out maybe??
|
||||||
if (actor.slide_mode === 'ice') {
|
if (actor.doomed)
|
||||||
// Actors can't make voluntary moves on ice
|
continue;
|
||||||
|
|
||||||
|
// Actors can't make voluntary moves on ice
|
||||||
|
if (actor.slide_mode === 'ice')
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
if (actor === this.player) {
|
if (actor === this.player) {
|
||||||
if (player_direction) {
|
if (player_direction) {
|
||||||
actor.direction = player_direction;
|
actor.direction = player_direction;
|
||||||
this.attempt_step(actor, 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
|
// bug behavior: always try turning as left as possible, and
|
||||||
// fall back to less-left turns when that fails
|
// fall back to less-left turns when that fails
|
||||||
let direction = DIRECTIONS[actor.direction].left;
|
let direction = DIRECTIONS[actor.direction].left;
|
||||||
@ -295,6 +301,48 @@ class Level {
|
|||||||
direction = DIRECTIONS[direction].right;
|
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?
|
// TODO do i need to do this more aggressively?
|
||||||
if (this.state === 'success' || this.state === 'failure')
|
if (this.state === 'success' || this.state === 'failure')
|
||||||
@ -311,31 +359,37 @@ class Level {
|
|||||||
let move = DIRECTIONS[direction].movement;
|
let move = DIRECTIONS[direction].movement;
|
||||||
let goal_x = actor.x + move[0];
|
let goal_x = actor.x + move[0];
|
||||||
let goal_y = actor.y + move[1];
|
let goal_y = actor.y + move[1];
|
||||||
let goal_cell = this.cells[goal_y][goal_x];
|
|
||||||
|
|
||||||
let blocks;
|
let blocked;
|
||||||
goal_cell.each(tile => {
|
if (goal_x >= 0 && goal_x < this.width && goal_y >= 0 && goal_y < this.height) {
|
||||||
if (tile !== actor && tile.type.blocks) {
|
let goal_cell = this.cells[goal_y][goal_x];
|
||||||
if (actor.type.pushes && actor.type.pushes[tile.type.name]) {
|
goal_cell.each(tile => {
|
||||||
if (this.attempt_step(tile, direction))
|
if (tile !== actor && tile.type.blocks) {
|
||||||
// It moved out of the way!
|
if (actor.type.pushes && actor.type.pushes[tile.type.name]) {
|
||||||
return;
|
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)
|
else {
|
||||||
// It became something non-blocking!
|
// Hit the edge
|
||||||
return;
|
blocked = true;
|
||||||
}
|
}
|
||||||
blocks = true;
|
|
||||||
// XXX should i break here, or bump everything?
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (blocks) {
|
if (blocked) {
|
||||||
if (actor.slide_mode === 'ice') {
|
if (actor.slide_mode === 'ice') {
|
||||||
// Actors on ice turn around when they hit something
|
// Actors on ice turn around when they hit something
|
||||||
actor.direction = DIRECTIONS[DIRECTIONS[direction].left].left;
|
actor.direction = DIRECTIONS[direction].opposite;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -419,11 +473,13 @@ class Game {
|
|||||||
this.tileset = tileset;
|
this.tileset = tileset;
|
||||||
|
|
||||||
// TODO obey level options; allow overriding
|
// TODO obey level options; allow overriding
|
||||||
this.camera_size_x = 9;
|
this.viewport_size_x = 19;
|
||||||
this.camera_size_y = 9;
|
this.viewport_size_y = 19;
|
||||||
|
|
||||||
this.container = document.body;
|
document.body.innerHTML = GAME_UI_HTML;
|
||||||
this.container.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.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.nav_el = this.container.querySelector('.nav');
|
||||||
@ -433,6 +489,7 @@ class Game {
|
|||||||
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');
|
||||||
|
|
||||||
|
// Populate navigation
|
||||||
this.nav_prev_button = this.nav_el.querySelector('.nav-prev');
|
this.nav_prev_button = this.nav_el.querySelector('.nav-prev');
|
||||||
this.nav_next_button = this.nav_el.querySelector('.nav-next');
|
this.nav_next_button = this.nav_el.querySelector('.nav-next');
|
||||||
this.nav_prev_button.addEventListener('click', ev => {
|
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_el.append(this.level_canvas);
|
||||||
this.level_canvas.setAttribute('tabindex', '-1');
|
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;
|
let last_key;
|
||||||
this.pending_player_move = null;
|
this.pending_player_move = null;
|
||||||
this.next_player_move = null;
|
this.next_player_move = null;
|
||||||
this.player_used_move = false;
|
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
|
// TODO this could all probably be more rigorous but it's fine for now
|
||||||
key_target.addEventListener('keydown', ev => {
|
key_target.addEventListener('keydown', ev => {
|
||||||
let direction;
|
let direction;
|
||||||
@ -495,6 +564,8 @@ class Game {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Done with UI, now we can load a level
|
||||||
|
this.load_level(0);
|
||||||
this.redraw();
|
this.redraw();
|
||||||
|
|
||||||
this.frame = 0;
|
this.frame = 0;
|
||||||
@ -537,6 +608,16 @@ class Game {
|
|||||||
requestAnimationFrame(this.do_frame.bind(this));
|
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() {
|
update_ui() {
|
||||||
// TODO can we do this only if they actually changed?
|
// 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;
|
||||||
@ -548,17 +629,30 @@ class Game {
|
|||||||
else {
|
else {
|
||||||
this.bummer_el.textContent = '';
|
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() {
|
redraw() {
|
||||||
let ctx = this.level_canvas.getContext('2d');
|
let ctx = this.level_canvas.getContext('2d');
|
||||||
ctx.clearRect(0, 0, this.level_canvas.width, this.level_canvas.height);
|
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 xmargin = (this.viewport_size_x - 1) / 2;
|
||||||
let camera_y = this.level.player.y - (this.camera_size_y - 1) / 2;
|
let ymargin = (this.viewport_size_y - 1) / 2;
|
||||||
for (let dx = 0; dx < this.camera_size_x; dx++) {
|
let x0 = this.level.player.x - xmargin;
|
||||||
for (let dy = 0; dy < this.camera_size_y; dy++) {
|
let y0 = this.level.player.y - ymargin;
|
||||||
let cell = this.level.cells[dy + camera_y][dx + camera_x];
|
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)
|
if (! cell.is_dirty)
|
||||||
continue;
|
continue;
|
||||||
@ -626,6 +720,10 @@ async function main() {
|
|||||||
stored_game = dat.parse_game(await fetch('levels/CCLP1.ccl'));
|
stored_game = dat.parse_game(await fetch('levels/CCLP1.ccl'));
|
||||||
}
|
}
|
||||||
let game = new Game(stored_game, tileset);
|
let game = new Game(stored_game, tileset);
|
||||||
|
|
||||||
|
if (query.get('debug')) {
|
||||||
|
game.debug = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|||||||
@ -1,27 +1,42 @@
|
|||||||
export const CC2_TILESET_LAYOUT = {
|
export const CC2_TILESET_LAYOUT = {
|
||||||
floor: [0, 2],
|
floor: [0, 2],
|
||||||
|
floor_letter: [2, 2],
|
||||||
wall: [1, 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: [
|
fire: [
|
||||||
[12, 29],
|
[12, 29],
|
||||||
[13, 29],
|
[13, 29],
|
||||||
[14, 29],
|
[14, 29],
|
||||||
[15, 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_n: [[0, 19], [0, 20]],
|
||||||
force_floor_e: [[2, 19], [2, 20]],
|
force_floor_e: [[2, 19], [2, 20]],
|
||||||
force_floor_s: [[1, 19], [1, 20]],
|
force_floor_s: [[1, 19], [1, 20]],
|
||||||
force_floor_w: [[3, 19], [3, 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: [
|
exit: [
|
||||||
[6, 2],
|
[6, 2],
|
||||||
@ -72,6 +87,11 @@ export const CC2_TILESET_LAYOUT = {
|
|||||||
flippers: [0, 6],
|
flippers: [0, 6],
|
||||||
|
|
||||||
hint: [5, 2],
|
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 = {
|
export const TILE_WORLD_TILESET_LAYOUT = {
|
||||||
@ -200,6 +220,7 @@ export class Tileset {
|
|||||||
draw(tile, ctx, x, y) {
|
draw(tile, ctx, x, y) {
|
||||||
let drawspec = this.layout[tile.type.name];
|
let drawspec = this.layout[tile.type.name];
|
||||||
let coords = drawspec;
|
let coords = drawspec;
|
||||||
|
if (! coords) console.error(tile.type.name);
|
||||||
if (!(coords instanceof Array)) {
|
if (!(coords instanceof Array)) {
|
||||||
// Must be an object of directions
|
// Must be an object of directions
|
||||||
coords = coords[tile.direction ?? 'south'];
|
coords = coords[tile.direction ?? 'south'];
|
||||||
|
|||||||
506
js/tiletypes.js
506
js/tiletypes.js
@ -1,15 +1,10 @@
|
|||||||
export const TILE_TYPES = {
|
const TILE_TYPES = {
|
||||||
cloner: {
|
// Floors and walls
|
||||||
blocks: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
floor: {
|
floor: {
|
||||||
cc2_byte: 0x01,
|
},
|
||||||
|
floor_letter: {
|
||||||
},
|
},
|
||||||
wall: {
|
wall: {
|
||||||
cc2_byte: 0x02,
|
|
||||||
blocks: true,
|
blocks: true,
|
||||||
},
|
},
|
||||||
wall_invisible: {
|
wall_invisible: {
|
||||||
@ -18,6 +13,8 @@ export const TILE_TYPES = {
|
|||||||
wall_appearing: {
|
wall_appearing: {
|
||||||
blocks: true,
|
blocks: true,
|
||||||
},
|
},
|
||||||
|
popwall: {
|
||||||
|
},
|
||||||
thinwall_n: {
|
thinwall_n: {
|
||||||
thin_walls: new Set(['north']),
|
thin_walls: new Set(['north']),
|
||||||
},
|
},
|
||||||
@ -43,78 +40,77 @@ export const TILE_TYPES = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
ice: {
|
// Swivel doors
|
||||||
cc2_byte: 0x03,
|
swivel_ne: {
|
||||||
on_arrive(me, level, other) {
|
thin_walls: new Set(['north'], ['east']),
|
||||||
level.make_slide(other, 'ice');
|
},
|
||||||
|
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: {
|
door_blue: {
|
||||||
cc2_byte: 0x04,
|
blocks: true,
|
||||||
thin_walls: {
|
on_bump(me, level, other) {
|
||||||
south: true,
|
if (other.type.has_inventory && other.take_item('key_blue')) {
|
||||||
west: true,
|
me.type = TILE_TYPES.floor;
|
||||||
},
|
|
||||||
on_arrive(me, level, other) {
|
|
||||||
if (other.direction === 'south') {
|
|
||||||
other.direction = 'east';
|
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
other.direction = 'north';
|
|
||||||
}
|
|
||||||
level.make_slide(other, 'ice');
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ice_nw: {
|
door_yellow: {
|
||||||
cc2_byte: 0x05,
|
blocks: true,
|
||||||
thin_walls: {
|
on_bump(me, level, other) {
|
||||||
north: true,
|
if (other.type.has_inventory && other.take_item('key_yellow')) {
|
||||||
west: true,
|
me.type = TILE_TYPES.floor;
|
||||||
},
|
|
||||||
on_arrive(me, level, other) {
|
|
||||||
if (other.direction === 'north') {
|
|
||||||
other.direction = 'east';
|
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
other.direction = 'south';
|
|
||||||
}
|
|
||||||
level.make_slide(other, 'ice');
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ice_ne: {
|
door_green: {
|
||||||
cc2_byte: 0x06,
|
blocks: true,
|
||||||
thin_walls: {
|
on_bump(me, level, other) {
|
||||||
north: true,
|
if (other.type.has_inventory && other.take_item('key_green')) {
|
||||||
east: true,
|
me.type = TILE_TYPES.floor;
|
||||||
},
|
|
||||||
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,
|
// Terrain
|
||||||
thin_walls: {
|
dirt: {
|
||||||
south: true,
|
// TODO block monsters, and melinda only without the hiking boots
|
||||||
east: true,
|
|
||||||
},
|
|
||||||
on_arrive(me, level, other) {
|
on_arrive(me, level, other) {
|
||||||
if (other.direction === 'south') {
|
me.become('floor');
|
||||||
other.direction = 'west';
|
}
|
||||||
|
},
|
||||||
|
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 {
|
else {
|
||||||
other.direction = 'north';
|
other.destroy();
|
||||||
}
|
}
|
||||||
level.make_slide(other, 'ice');
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
water: {
|
water: {
|
||||||
cc2_byte: 0x08,
|
|
||||||
on_arrive(me, level, other) {
|
on_arrive(me, level, other) {
|
||||||
// TODO cc1 allows items under water, i think; water was on the upper layer
|
// TODO cc1 allows items under water, i think; water was on the upper layer
|
||||||
if (other.type.name == 'dirt_block') {
|
if (other.type.name == 'dirt_block') {
|
||||||
@ -130,58 +126,226 @@ export const TILE_TYPES = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fire: {
|
turtle: {
|
||||||
cc2_byte: 0x09,
|
},
|
||||||
|
ice: {
|
||||||
on_arrive(me, level, other) {
|
on_arrive(me, level, other) {
|
||||||
if (other.type.is_player) {
|
level.make_slide(other, 'ice');
|
||||||
level.fail("Oops! You can't walk on fire without fire boots!");
|
}
|
||||||
other.become('player_burned');
|
},
|
||||||
|
ice_sw: {
|
||||||
|
thin_walls: {
|
||||||
|
south: true,
|
||||||
|
west: true,
|
||||||
|
},
|
||||||
|
on_arrive(me, level, other) {
|
||||||
|
if (other.direction === 'south') {
|
||||||
|
other.direction = 'east';
|
||||||
}
|
}
|
||||||
else {
|
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: {
|
force_floor_n: {
|
||||||
cc2_byte: 0x0a,
|
|
||||||
on_arrive(me, level, other) {
|
on_arrive(me, level, other) {
|
||||||
other.direction = 'north';
|
other.direction = 'north';
|
||||||
level.make_slide(other, 'push');
|
level.make_slide(other, 'push');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
force_floor_e: {
|
force_floor_e: {
|
||||||
cc2_byte: 0x0b,
|
|
||||||
on_arrive(me, level, other) {
|
on_arrive(me, level, other) {
|
||||||
other.direction = 'east';
|
other.direction = 'east';
|
||||||
level.make_slide(other, 'push');
|
level.make_slide(other, 'push');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
force_floor_s: {
|
force_floor_s: {
|
||||||
cc2_byte: 0x0c,
|
|
||||||
on_arrive(me, level, other) {
|
on_arrive(me, level, other) {
|
||||||
other.direction = 'south';
|
other.direction = 'south';
|
||||||
level.make_slide(other, 'push');
|
level.make_slide(other, 'push');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
force_floor_w: {
|
force_floor_w: {
|
||||||
cc2_byte: 0x0d,
|
|
||||||
on_arrive(me, level, other) {
|
on_arrive(me, level, other) {
|
||||||
other.direction = 'west';
|
other.direction = 'west';
|
||||||
level.make_slide(other, 'push');
|
level.make_slide(other, 'push');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
bomb: {
|
||||||
exit: {
|
},
|
||||||
cc2_byte: 0x14,
|
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: {
|
player: {
|
||||||
cc2_byte: 0x16,
|
|
||||||
is_actor: true,
|
is_actor: true,
|
||||||
is_player: true,
|
is_player: true,
|
||||||
has_inventory: true,
|
has_inventory: true,
|
||||||
has_direction: true,
|
is_object: true,
|
||||||
is_top_layer: true,
|
|
||||||
pushes: {
|
pushes: {
|
||||||
dirt_block: true,
|
dirt_block: true,
|
||||||
},
|
},
|
||||||
@ -190,77 +354,11 @@ export const TILE_TYPES = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
player_drowned: {
|
player_drowned: {
|
||||||
cc2_byte: null,
|
|
||||||
},
|
},
|
||||||
player_burned: {
|
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: {
|
chip: {
|
||||||
cc2_byte: 0x2a,
|
is_object: true,
|
||||||
is_top_layer: true,
|
|
||||||
is_chip: true,
|
is_chip: true,
|
||||||
is_required_chip: true,
|
is_required_chip: true,
|
||||||
on_arrive(me, level, other) {
|
on_arrive(me, level, other) {
|
||||||
@ -271,12 +369,25 @@ export const TILE_TYPES = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
chip_extra: {
|
chip_extra: {
|
||||||
cc2_byte: 0x2b,
|
|
||||||
is_chip: true,
|
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: {
|
socket: {
|
||||||
cc2_byte: 0x2c,
|
|
||||||
blocks: true,
|
blocks: true,
|
||||||
on_bump(me, level, other) {
|
on_bump(me, level, other) {
|
||||||
if (other.type.is_player && level.chips_remaining === 0) {
|
if (other.type.is_player && level.chips_remaining === 0) {
|
||||||
@ -284,98 +395,13 @@ export const TILE_TYPES = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
exit: {
|
||||||
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,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Tell them all their own names
|
||||||
export const CC2_TILE_TYPES = new Array(256);
|
for (let [name, type] of Object.entries(TILE_TYPES)) {
|
||||||
CC2_TILE_TYPES.fill(null);
|
type.name = name;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default TILE_TYPES;
|
||||||
|
|||||||
24
style.css
24
style.css
@ -24,6 +24,12 @@ main {
|
|||||||
/ min-content 12em
|
/ min-content 12em
|
||||||
;
|
;
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
|
|
||||||
|
image-rendering: optimizeSpeed;
|
||||||
|
|
||||||
|
--tile-width: 32px;
|
||||||
|
--tile-height: 32px;
|
||||||
|
--scale: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.level {
|
.level {
|
||||||
@ -33,9 +39,7 @@ main {
|
|||||||
}
|
}
|
||||||
.level canvas {
|
.level canvas {
|
||||||
display: block;
|
display: block;
|
||||||
width: calc(9 * 32px * 2);
|
width: calc(9 * var(--tile-width) * var(--scale));
|
||||||
width: calc(9 * 48px * 2);
|
|
||||||
image-rendering: optimizeSpeed;
|
|
||||||
}
|
}
|
||||||
.meta {
|
.meta {
|
||||||
grid-area: meta;
|
grid-area: meta;
|
||||||
@ -55,9 +59,13 @@ main {
|
|||||||
.chips {
|
.chips {
|
||||||
grid-area: chips;
|
grid-area: chips;
|
||||||
|
|
||||||
|
padding: 0 0.5em;
|
||||||
color: yellow;
|
color: yellow;
|
||||||
background: black;
|
background: black;
|
||||||
}
|
}
|
||||||
|
.chips::before {
|
||||||
|
content: "chips left: ";
|
||||||
|
}
|
||||||
.time {
|
.time {
|
||||||
grid-area: time;
|
grid-area: time;
|
||||||
}
|
}
|
||||||
@ -66,6 +74,16 @@ main {
|
|||||||
}
|
}
|
||||||
.inventory {
|
.inventory {
|
||||||
grid-area: 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 {
|
.bummer {
|
||||||
grid-area: level;
|
grid-area: level;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user