lexys-labyrinth/js/tileset.js

3161 lines
98 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { DIRECTIONS } from './defs.js';
import TILE_TYPES from './tiletypes.js';
const _omit_custom_lexy_vfx = {
teleport_flash: null,
transmogrify_flash: null,
puff: null,
fall: null,
};
export const CC2_TILESET_LAYOUT = {
'#ident': 'cc2',
'#name': "Chip's Challenge 2",
'#dimensions': [16, 32],
'#transparent-color': [0, 0],
'#supported-versions': new Set(['cc1', 'cc2']),
'#wire-width': 1/16,
'#editor-arrows': {
north: [14, 31],
east: [14.5, 31],
south: [15, 31],
west: [15.5, 31],
},
door_red: [0, 1],
door_blue: [1, 1],
door_yellow: [2, 1],
door_green: [3, 1],
key_red: [4, 1],
key_blue: [5, 1],
key_yellow: [6, 1],
key_green: [7, 1],
dirt_block: {
__special__: 'perception',
modes: new Set(['editor', 'xray']),
hidden: [8, 1],
revealed: [9, 1],
},
ice: [10, 1],
ice_se: [11, 1],
ice_sw: [12, 1],
ice_ne: [13, 1],
ice_nw: [14, 1],
cloner: [15, 1],
floor: {
// Wiring!
__special__: 'wires',
base: [0, 2],
wired: [8, 26],
wired_cross: [10, 26],
is_wired_optional: true,
},
wall_invisible: {
__special__: 'perception',
modes: new Set(['palette', 'editor', 'xray']),
hidden: [0, 2],
revealed: [9, 31],
},
wall_invisible_revealed: {
// This is specifically /invisible/ when you have the xray glasses
__special__: 'perception',
modes: new Set(['xray']),
hidden: [1, 2],
revealed: null,
},
wall_appearing: {
__special__: 'perception',
modes: new Set(['palette', 'editor', 'xray']),
hidden: [0, 2],
revealed: [11, 31],
},
wall: [1, 2],
floor_letter: {
__special__: 'letter',
base: [2, 2],
letter_glyphs: {
// Arrows
"⬆": [14, 31],
"➡": [14.5, 31],
"⬇": [15, 31],
"⬅": [15.5, 31],
},
letter_ranges: [{
// ASCII text (only up through uppercase)
range: [32, 96],
x0: 0,
y0: 0,
w: 0.5,
h: 0.5,
columns: 32,
}],
},
thief_tools: [3, 2],
socket: [4, 2],
hint: [5, 2],
exit: {
__special__: 'animated',
duration: 16,
all: [[6, 2], [7, 2], [8, 2], [9, 2]],
},
ice_block: {
__special__: 'perception',
modes: new Set(['editor', 'xray']),
hidden: [10, 2],
revealed: [11, 2],
},
score_1000: [12, 2],
score_100: [13, 2],
score_10: [14, 2],
score_2x: [15, 2],
// LCD digit font
green_chip: [9, 3],
chip_extra: {
__special__: 'perception',
modes: new Set(['palette', 'editor']),
hidden: [11, 3],
revealed: [10, 3],
},
chip: [11, 3],
bribe: [12, 3],
speed_boots: [13, 3],
canopy: {
__special__: 'perception',
modes: new Set(['editor', 'xray']),
hidden: [14, 3],
revealed: [15, 3],
},
dynamite: [0, 4],
dynamite_lit: {
__special__: 'visual-state',
0: [0, 4],
1: [1, 4],
2: [2, 4],
3: [3, 4],
4: [4, 4],
},
bomb: {
__special__: 'bomb-fuse',
bomb: [5, 4],
fuse: [7, 4],
},
green_bomb: {
__special__: 'bomb-fuse',
bomb: [6, 4],
fuse: [7, 4],
},
floor_custom_green: [8, 4],
floor_custom_pink: [9, 4],
floor_custom_yellow: [10, 4],
floor_custom_blue: [11, 4],
wall_custom_green: [12, 4],
wall_custom_pink: [13, 4],
wall_custom_yellow: [14, 4],
wall_custom_blue: [15, 4],
explosion: [[0, 5], [1, 5], [2, 5], [3, 5]],
explosion_nb: [[0, 5], [1, 5], [2, 5], [3, 5]],
splash_slime: [[0, 5], [1, 5], [2, 5], [3, 5]],
splash: [[4, 5], [5, 5], [6, 5], [7, 5]],
flame_jet_off: [8, 5],
flame_jet_on: {
__special__: 'animated',
duration: 12,
all: [[9, 5], [10, 5], [11, 5]],
},
popdown_wall: [12, 5],
popdown_floor: {
__special__: 'perception',
modes: new Set(['palette', 'editor', 'xray']),
hidden: {
__special__: 'visual-state',
depressed: [13, 5],
normal: [12, 5],
},
revealed: [13, 5],
},
no_sign: [14, 5],
frame_block: {
__special__: 'arrows',
base: [15, 5],
arrows: [3, 10],
},
flippers: [0, 6],
fire_boots: [1, 6],
cleats: [2, 6],
suction_boots: [3, 6],
hiking_boots: [4, 6],
lightning_bolt: [5, 6],
'#active-player-background': [6, 6],
// TODO dopps can push but i don't think they have any other visuals
doppelganger1: {
__special__: 'overlay',
base: [7, 6],
overlay: 'player',
},
doppelganger2: {
__special__: 'overlay',
base: [7, 6],
overlay: 'player2',
},
button_blue: [8, 6],
button_green: [9, 6],
button_red: [10, 6],
button_brown: [11, 6],
button_pink: {
__special__: 'wires',
base: [0, 2],
wired: [12, 6],
},
button_black: {
__special__: 'wires',
base: [0, 2],
wired: [13, 6],
},
button_orange: [14, 6],
button_yellow: [15, 6],
// TODO moving
bug: {
__special__: 'animated',
global: false,
duration: 1,
north: [[0, 7], [1, 7], [2, 7], [3, 7]],
east: [[4, 7], [5, 7], [6, 7], [7, 7]],
south: [[8, 7], [9, 7], [10, 7], [11, 7]],
west: [[12, 7], [13, 7], [14, 7], [15, 7]],
},
tank_blue: {
__special__: 'animated',
duration: 20,
cc2_duration: 32,
north: [[0, 8], [1, 8]],
east: [[2, 8], [3, 8]],
south: [[4, 8], [5, 8]],
west: [[6, 8], [7, 8]],
},
glider: {
__special__: 'animated',
duration: 10,
cc2_duration: 8,
north: [[8, 8], [9, 8]],
east: [[10, 8], [11, 8]],
south: [[12, 8], [13, 8]],
west: [[14, 8], [15, 8]],
},
green_floor: {
__special__: 'animated',
duration: 24,
cc2_duration: 16,
all: [[0, 9], [1, 9], [2, 9], [3, 9]],
},
purple_floor: {
__special__: 'animated',
duration: 24,
cc2_duration: 16,
all: [[4, 9], [5, 9], [6, 9], [7, 9]],
},
green_wall: {
__special__: 'overlay',
base: 'green_floor',
overlay: [8, 9],
},
purple_wall: {
__special__: 'overlay',
base: 'purple_floor',
overlay: [8, 9],
},
trap: {
__special__: 'visual-state',
closed: [9, 9],
open: [10, 9],
},
button_gray: [11, 9],
fireball: {
__special__: 'animated',
duration: 12,
cc2_duration: 4,
all: [[12, 9], [13, 9], [14, 9], [15, 9]],
},
fake_wall: [0, 10],
fake_floor: {
__special__: 'perception',
modes: new Set(['palette', 'editor', 'xray']),
hidden: [0, 10],
revealed: [10, 31],
},
// Thin walls are built piecemeal from two tiles; the first is N/S, the second is E/W
thin_walls: {
__special__: 'thin_walls',
thin_walls_ns: [1, 10],
thin_walls_ew: [2, 10],
},
teleport_blue: {
__special__: 'wires',
base: [0, 2],
wired: {
__special__: 'animated',
duration: 20,
cc2_duration: 16,
all: [[4, 10], [5, 10], [6, 10], [7, 10]],
},
},
popwall: [8, 10],
popwall2: [8, 10],
gravel: [9, 10],
ball: {
__special__: 'animated',
global: false,
duration: 0.5,
cc2_duration: 1,
idle_frame_index: 2,
// appropriately, this animation ping-pongs
all: [[10, 10], [11, 10], [12, 10], [13, 10], [14, 10], [13, 10], [12, 10], [11, 10]],
},
steel: {
// Wiring!
__special__: 'wires',
base: [15, 10],
wired: [9, 26],
wired_cross: [11, 26],
is_wired_optional: true,
},
teeth: {
__special__: 'animated',
global: false,
duration: 1,
idle_frame_index: 1,
// NOTE: CC2 inexplicably dropped north teeth and just uses the south sprites instead
north: [[0, 11], [1, 11], [2, 11], [1, 11]],
east: [[3, 11], [4, 11], [5, 11], [4, 11]],
south: [[0, 11], [1, 11], [2, 11], [1, 11]],
west: [[6, 11], [7, 11], [8, 11], [7, 11]],
},
swivel_sw: [9, 11],
swivel_nw: [10, 11],
swivel_ne: [11, 11],
swivel_se: [12, 11],
swivel_floor: [13, 11],
'#wire-tunnel': [14, 11],
stopwatch_penalty: [15, 11],
paramecium: {
__special__: 'animated',
global: false,
duration: 1,
north: [[0, 12], [1, 12], [2, 12]],
east: [[3, 12], [4, 12], [5, 12]],
south: [[6, 12], [7, 12], [8, 12]],
west: [[9, 12], [10, 12], [11, 12]],
},
foil: [12, 12],
turtle: {
// Turtles draw atop fake water, but don't act like water otherwise
__special__: 'overlay',
overlay: {
__special__: 'animated',
duration: 256,
positionally_hashed: true,
all: [[13, 12], [14, 12], [15, 12], [14, 12]],
},
base: 'water',
},
walker: {
__special__: 'double-size-monster',
base: [0, 13],
vertical: [null, [1, 13], [2, 13], [3, 13], [4, 13], [5, 13], [6, 13], [7, 13]],
horizontal: [null, [8, 13], [10, 13], [12, 13], [14, 13], [8, 14], [10, 14], [12, 14]],
},
helmet: [0, 14],
stopwatch_toggle: [14, 14],
stopwatch_bonus: [15, 14],
blob: {
__special__: 'double-size-monster',
base: [0, 15],
vertical: [null, [1, 15], [2, 15], [3, 15], [4, 15], [5, 15], [6, 15], [7, 15]],
horizontal: [null, [8, 15], [10, 15], [12, 15], [14, 15], [8, 16], [10, 16], [12, 16]],
},
// (cc2 editor copy/paste outline)
floor_mimic: {
__special__: 'perception',
modes: new Set(['palette', 'editor', 'xray']),
hidden: [0, 2],
revealed: [14, 16],
},
// (cc2 editor cursor outline)
// timid teeth
teeth_timid: {
__special__: 'animated',
global: false,
duration: 1,
idle_frame_index: 1,
// NOTE: CC2 inexplicably dropped north teeth and just uses the south sprites instead
// NOTE: it also skimped on timid teeth frames
north: [[1, 17], [0, 17]],
east: [[3, 17], [2, 17]],
south: [[1, 17], [0, 17]],
west: [[5, 17], [4, 17]],
},
bowling_ball: [6, 17],
rolling_ball: {
__special__: 'animated',
global: false,
duration: 2,
all: [[6, 17], [7, 17]],
},
tank_yellow: {
__special__: 'animated',
duration: 20,
cc2_duration: 32,
north: [[8, 17], [9, 17]],
east: [[10, 17], [11, 17]],
south: [[12, 17], [13, 17]],
west: [[14, 17], [15, 17]],
},
rover: {
__special__: 'rover',
direction: [10, 18],
inert: [0, 18],
teeth: {
__special__: 'animated',
duration: 16,
all: [[0, 18], [8, 18]],
},
// cw, slow
glider: {
__special__: 'animated',
duration: 32,
all: [[0, 18], [1, 18], [2, 18], [3, 18], [4, 18], [5, 18], [6, 18], [7, 18]],
},
// ccw, fast
bug: {
__special__: 'animated',
duration: 16,
all: [[7, 18], [6, 18], [5, 18], [4, 18], [3, 18], [2, 18], [1, 18], [0, 18]],
},
ball: {
__special__: 'animated',
duration: 16,
all: [[0, 18], [4, 18]],
},
teeth_timid: {
__special__: 'animated',
duration: 16,
all: [[0, 18], [9, 18]],
},
// ccw, slow
fireball: {
__special__: 'animated',
duration: 32,
all: [[7, 18], [6, 18], [5, 18], [4, 18], [3, 18], [2, 18], [1, 18], [0, 18]],
},
// cw, fast
paramecium: {
__special__: 'animated',
duration: 16,
all: [[0, 18], [1, 18], [2, 18], [3, 18], [4, 18], [5, 18], [6, 18], [7, 18]],
},
walker: {
__special__: 'animated',
duration: 16,
all: [[8, 18], [9, 18]],
},
},
xray_eye: [11, 18],
ghost: {
north: [12, 18],
east: [13, 18],
south: [14, 18],
west: [15, 18],
},
force_floor_n: {
__special__: 'scroll',
duration: 24,
cc2_duration: 8,
base: [0, 19],
scroll_region: [0, 1],
},
force_floor_e: {
__special__: 'scroll',
duration: 24,
cc2_duration: 8,
base: [3, 19],
scroll_region: [-1, 0],
},
force_floor_s: {
__special__: 'scroll',
duration: 24,
cc2_duration: 8,
base: [1, 20],
scroll_region: [0, -1],
},
force_floor_w: {
__special__: 'scroll',
duration: 24,
cc2_duration: 8,
base: [2, 20],
scroll_region: [1, 0],
},
teleport_green: {
__special__: 'animated',
duration: 20,
cc2_duration: 16,
// Nice little touch: green teleporters aren't animated in sync
positionally_hashed: true,
all: [[4, 19], [5, 19], [6, 19], [7, 19]],
},
teleport_yellow: {
__special__: 'animated',
duration: 20,
cc2_duration: 16,
all: [[8, 19], [9, 19], [10, 19], [11, 19]],
},
transmogrifier: {
__special__: 'visual-state',
active: {
__special__: 'animated',
duration: 16,
all: [[12, 19], [13, 19], [14, 19], [15, 19]],
},
inactive: [12, 19],
},
teleport_red: {
__special__: 'wires',
base: [0, 2],
wired: {
__special__: 'visual-state',
active: {
__special__: 'animated',
duration: 20,
cc2_duration: 16,
all: [[4, 20], [5, 20], [6, 20], [7, 20]],
},
inactive: [4, 20],
},
},
slime: {
__special__: 'animated',
duration: 60,
all: [[8, 20], [9, 20], [10, 20], [11, 20], [12, 20], [13, 20], [14, 20], [15, 20]],
},
force_floor_all: {
__special__: 'animated',
duration: 24,
cc2_duration: 8,
all: [[0, 21], [1, 21], [2, 21], [3, 21], [4, 21], [5, 21], [6, 21], [7, 21]],
},
// latches
light_switch_off: {
__special__: 'wires',
base: [14, 21],
wired: [12, 21],
},
light_switch_on: {
__special__: 'wires',
base: [14, 21],
wired: [13, 21],
},
thief_keys: [15, 21],
player: {
__special__: 'visual-state',
normal: {
north: [0, 22],
south: [0, 23],
west: [8, 23],
east: [8, 22],
},
blocked: {
north: [8, 24],
east: [9, 24],
south: [10, 24],
west: [11, 24],
},
moving: {
__special__: 'animated',
global: false,
duration: 0.5,
cc2_duration: 1,
north: [[0, 22], [1, 22], [2, 22], [3, 22], [4, 22], [5, 22], [6, 22], [7, 22]],
east: [[8, 22], [9, 22], [10, 22], [11, 22], [12, 22], [13, 22], [14, 22], [15, 22]],
south: [[0, 23], [1, 23], [2, 23], [3, 23], [4, 23], [5, 23], [6, 23], [7, 23]],
west: [[8, 23], [9, 23], [10, 23], [11, 23], [12, 23], [13, 23], [14, 23], [15, 23]],
},
pushing: 'blocked',
swimming: {
__special__: 'animated',
global: false,
duration: 1,
north: [[0, 24], [1, 24]],
east: [[2, 24], [3, 24]],
south: [[4, 24], [5, 24]],
west: [[6, 24], [7, 24]],
},
// The classic CC2 behavior, spinning/slipping on ice
skating: {
__special__: 'animated',
global: false,
duration: 0.5,
all: [
[0, 22], [1, 22], [2, 22], [8, 22], [9, 22], [10, 22],
[0, 23], [1, 23], [2, 23], [8, 23], [9, 23], [10, 23],
],
},
// TODO i don't know what CC2 does
forced: {
north: [2, 22],
east: [10, 22],
south: [2, 23],
west: [10, 23],
},
exited: 'normal',
// These are frames from the splash/explosion animations
drowned: [5, 5],
slimed: [5, 5],
burned: [1, 5],
exploded: [1, 5],
failed: [1, 5],
fell: [5, 39],
},
// Do a quick spin I guess??
player1_exit: [[0, 22], [8, 22], [0, 23], [8, 23]],
bogus_player_win: {
__special__: 'overlay',
overlay: [0, 23],
base: 'exit',
},
bogus_player_swimming: {
north: [0, 24],
east: [2, 24],
south: [4, 24],
west: [6, 24],
},
bogus_player_drowned: {
__special__: 'overlay',
overlay: [5, 5], // splash
base: 'water',
},
bogus_player_burned_fire: {
__special__: 'overlay',
overlay: [2, 5], // explosion frame 3
base: 'fire',
},
bogus_player_burned: {
__special__: 'overlay',
overlay: [2, 5], // explosion frame 3
base: 'floor',
},
water: {
__special__: 'animated',
duration: 36,
cc2_duration: 20,
all: [[12, 24], [13, 24], [14, 24], [15, 24]],
},
logic_gate: {
__special__: 'logic-gate',
counter_numbers: {
x: 0,
y: 3,
width: 0.75,
height: 1,
},
logic_gate_tiles: {
'latch-ccw': {
north: [8, 21],
east: [9, 21],
south: [10, 21],
west: [11, 21],
},
not: {
north: [0, 25],
east: [1, 25],
south: [2, 25],
west: [3, 25],
},
and: {
north: [4, 25],
east: [5, 25],
south: [6, 25],
west: [7, 25],
},
or: {
north: [8, 25],
east: [9, 25],
south: [10, 25],
west: [11, 25],
},
xor: {
north: [12, 25],
east: [13, 25],
south: [14, 25],
west: [15, 25],
},
'latch-cw': {
north: [0, 26],
east: [1, 26],
south: [2, 26],
west: [3, 26],
},
nand: {
north: [4, 26],
east: [5, 26],
south: [6, 26],
west: [7, 26],
},
counter: [14, 26],
},
},
'#unpowered': [13, 26],
'#powered': [15, 26],
player2: {
__special__: 'visual-state',
normal: {
north: [0, 27],
south: [0, 28],
west: [8, 28],
east: [8, 27],
},
blocked: {
north: [8, 29],
east: [9, 29],
south: [10, 29],
west: [11, 29],
},
moving: {
__special__: 'animated',
global: false,
duration: 0.5,
cc2_duration: 1,
north: [[0, 27], [1, 27], [2, 27], [3, 27], [4, 27], [5, 27], [6, 27], [7, 27]],
south: [[0, 28], [1, 28], [2, 28], [3, 28], [4, 28], [5, 28], [6, 28], [7, 28]],
west: [[8, 28], [9, 28], [10, 28], [11, 28], [12, 28], [13, 28], [14, 28], [15, 28]],
east: [[8, 27], [9, 27], [10, 27], [11, 27], [12, 27], [13, 27], [14, 27], [15, 27]],
},
pushing: 'blocked',
swimming: {
__special__: 'animated',
global: false,
duration: 1,
north: [[0, 29], [1, 29]],
east: [[2, 29], [3, 29]],
south: [[4, 29], [5, 29]],
west: [[6, 29], [7, 29]],
},
// The classic CC2 behavior, spinning on ice (which can never happen but)
skating: {
__special__: 'animated',
global: false,
duration: 0.5,
all: [
[0, 27], [1, 27], [2, 27], [8, 27], [9, 27], [10, 27],
[0, 28], [1, 28], [2, 28], [8, 28], [9, 28], [10, 28],
],
},
// TODO i don't know what CC2 does
forced: {
north: [2, 27],
east: [10, 27],
south: [2, 28],
west: [10, 28],
},
exited: 'normal',
// These are frames from the splash/explosion animations
drowned: [5, 5],
slimed: [5, 5],
burned: [1, 5],
exploded: [1, 5],
failed: [1, 5],
fell: [5, 39],
},
player2_exit: [[0, 27], [8, 27], [0, 28], [8, 28]],
fire: {
__special__: 'animated',
duration: 36,
cc2_duration: 20,
all: [[12, 29], [13, 29], [14, 29], [15, 29]],
},
railroad: {
__special__: 'railroad',
base: [9, 10],
railroad_ties: {
ne: [0, 30],
se: [1, 30],
sw: [2, 30],
nw: [3, 30],
ew: [4, 30],
ns: [5, 30],
},
railroad_switch: [6, 30],
railroad_inactive: {
ne: [7, 30],
se: [8, 30],
sw: [9, 30],
nw: [10, 30],
ew: [11, 30],
ns: [12, 30],
},
railroad_active: {
ne: [13, 30],
se: [14, 30],
sw: [15, 30],
nw: [0, 31],
ew: [1, 31],
ns: [2, 31],
},
},
railroad_sign: [3, 31],
dirt: [4, 31],
no_player2_sign: [5, 31],
no_player1_sign: [6, 31],
hook: [7, 31],
// misc other stuff
..._omit_custom_lexy_vfx,
};
// (This is really the MSCC layout, but often truncated in such a way that only TW can use it)
export const TILE_WORLD_TILESET_LAYOUT = {
'#ident': 'tw-static',
'#name': "Tile World (static)",
'#dimensions': [7, 16],
'#transparent-color': [0xff, 0x00, 0xff, 0xff],
'#supported-versions': new Set(['cc1']),
floor: [0, 0],
wall: [0, 1],
chip: [0, 2],
water: [0, 3],
fire: [0, 4],
wall_invisible: [0, 5],
wall_invisible_revealed: [0, 1],
// FIXME in cc1 tilesets these are opaque so they should draw at the terrain layer
thin_walls: {
__special__: 'thin_walls_cc1',
north: [0, 6],
west: [0, 7],
south: [0, 8],
east: [0, 9],
southeast: [3, 0],
},
// This is the non-directed dirt block, which we don't have
// dirt_block: [0, 10],
dirt: [0, 11],
ice: [0, 12],
force_floor_s: [0, 13],
dirt_block: {
north: [0, 14],
west: [0, 15],
south: [1, 0],
east: [1, 1],
},
force_floor_n: [1, 2],
force_floor_e: [1, 3],
force_floor_w: [1, 4],
exit: [1, 5],
door_blue: [1, 6],
door_red: [1, 7],
door_green: [1, 8],
door_yellow: [1, 9],
ice_nw: [1, 10],
ice_ne: [1, 11],
ice_se: [1, 12],
ice_sw: [1, 13],
fake_floor: {
__special__: 'perception',
modes: new Set(['palette', 'editor', 'xray']),
hidden: [1, 15],
revealed: [1, 14],
},
fake_wall: [1, 15],
// TODO overlay buffer?? [2, 0]
thief_tools: [2, 1],
socket: [2, 2],
button_green: [2, 3],
button_red: [2, 4],
green_wall: [2, 5],
green_floor: [2, 6],
button_brown: [2, 7],
button_blue: [2, 8],
teleport_blue: [2, 9],
bomb: [2, 10],
trap: {
__special__: 'visual-state',
closed: [2, 11],
open: [2, 11],
},
wall_appearing: [2, 12],
gravel: [2, 13],
popwall: [2, 14],
popwall2: [2, 14],
hint: [2, 15],
cloner: [3, 1],
force_floor_all: [3, 2],
splash: [3, 3],
bogus_player_drowned: [3, 3],
bogus_player_burned_fire: [3, 4],
bogus_player_burned: [3, 5],
explosion: [3, 6],
explosion_nb: [3, 6],
explosion_other: [3, 7], // TODO ???
// 3, 8 unused
bogus_player_win: [3, 9], // TODO 10 and 11 too? does this animate?
bogus_player_swimming: {
north: [3, 12],
west: [3, 13],
south: [3, 14],
east: [3, 15],
},
bug: {
north: [4, 0],
west: [4, 1],
south: [4, 2],
east: [4, 3],
},
fireball: {
north: [4, 4],
west: [4, 5],
south: [4, 6],
east: [4, 7],
},
ball: {
north: [4, 8],
west: [4, 9],
south: [4, 10],
east: [4, 11],
},
tank_blue: {
north: [4, 12],
west: [4, 13],
south: [4, 14],
east: [4, 15],
},
glider: {
north: [5, 0],
west: [5, 1],
south: [5, 2],
east: [5, 3],
},
teeth: {
north: [5, 4],
west: [5, 5],
south: [5, 6],
east: [5, 7],
},
walker: {
north: [5, 8],
west: [5, 9],
south: [5, 10],
east: [5, 11],
},
blob: {
north: [5, 12],
west: [5, 13],
south: [5, 14],
east: [5, 15],
},
paramecium: {
north: [6, 0],
west: [6, 1],
south: [6, 2],
east: [6, 3],
},
key_blue: [6, 4],
key_red: [6, 5],
key_green: [6, 6],
key_yellow: [6, 7],
flippers: [6, 8],
fire_boots: [6, 9],
cleats: [6, 10],
suction_boots: [6, 11],
player: {
__special__: 'visual-state',
normal: {
north: [6, 12],
south: [6, 14],
west: [6, 13],
east: [6, 15],
},
moving: 'normal',
pushing: 'normal',
blocked: 'normal',
swimming: {
north: [3, 12],
west: [3, 13],
south: [3, 14],
east: [3, 15],
},
skating: 'normal',
forced: 'normal',
drowned: [3, 3],
burned: [3, 4], // TODO TW's lynx mode doesn't use this! it uses the generic failed
exploded: [3, 6],
failed: [3, 7],
},
..._omit_custom_lexy_vfx,
};
export const LL_TILESET_LAYOUT = {
'#ident': 'lexy',
'#name': "Lexy's Labyrinth",
'#dimensions': [32, 32],
'#supported-versions': new Set(['cc1', 'cc2', 'll']),
'#wire-width': 1/16,
'#editor-arrows': {
north: [25, 31],
east: [25.5, 31],
south: [25, 31.5],
west: [25.5, 31.5],
},
// ------------------------------------------------------------------------------------------------
// Left side: tiles
// Terrain
floor: {
// Wiring!
__special__: 'wires',
base: [0, 2],
wired: [0, 28],
wired_cross: [1, 28],
is_wired_optional: true,
},
wall: [0, 3],
floor_letter: {
__special__: 'letter',
base: [1, 2],
letter_glyphs: {
// Arrows
"⬆": [6, 1],
"➡": [6.5, 1],
"⬇": [6, 1.5],
"⬅": [6.5, 1.5],
},
letter_ranges: [{
// ASCII text (only up through uppercase)
range: [32, 96],
x0: 0,
y0: 0,
w: 0.5,
h: 0.5,
columns: 32,
}],
},
steel: {
// Wiring!
__special__: 'wires',
base: [1, 3],
wired: [0, 29],
wired_cross: [1, 29],
is_wired_optional: true,
},
hint: [2, 2],
wall_invisible: {
__special__: 'perception',
modes: new Set(['palette', 'editor', 'xray']),
hidden: [0, 2],
revealed: [3, 2],
},
wall_invisible_revealed: {
// This is specifically /invisible/ when you have the xray glasses
__special__: 'perception',
modes: new Set(['xray']),
hidden: [0, 3],
revealed: null,
},
wall_appearing: {
__special__: 'perception',
modes: new Set(['palette', 'editor', 'xray']),
hidden: [0, 2],
revealed: [3, 3],
},
popwall: [4, 2],
popwall2: [4, 3],
fake_floor: {
__special__: 'perception',
modes: new Set(['palette', 'editor', 'xray']),
hidden: [5, 3],
revealed: [5, 2],
},
fake_wall: [5, 3],
popdown_floor: {
__special__: 'perception',
modes: new Set(['palette', 'editor', 'xray']),
hidden: {
__special__: 'visual-state',
depressed: [6, 2],
normal: [6, 3],
},
revealed: [6, 2],
},
popdown_wall: [6, 3],
thief_tools: [8, 2],
thief_keys: [8, 3],
canopy: {
__special__: 'perception',
modes: new Set(['editor', 'xray']),
hidden: [9, 2],
revealed: [9, 3],
},
no_player1_sign: [10, 2],
no_player2_sign: [10, 3],
socket: [11, 2],
exit: {
__special__: 'animated',
duration: 16,
all: [[12, 2], [13, 2], [14, 2], [15, 2]],
},
'#active-player-background': {
__special__: 'animated',
duration: 120,
all: [[12, 3], [13, 3]],
},
// TODO dopps can push but i don't think they have any other visuals
doppelganger1: {
__special__: 'overlay',
base: [14, 3],
overlay: 'player',
},
doppelganger2: {
__special__: 'overlay',
base: [14, 3],
overlay: 'player2',
},
'#killer-indicator': [15, 3],
floor_custom_green: [0, 4],
floor_custom_pink: [1, 4],
floor_custom_yellow: [2, 4],
floor_custom_blue: [3, 4],
wall_custom_green: [0, 5],
wall_custom_pink: [1, 5],
wall_custom_yellow: [2, 5],
wall_custom_blue: [3, 5],
sand: [0, 6],
spikes: [0, 7],
hole: {
__special__: 'visual-state',
north: [1, 6],
open: [1, 7],
},
cracked_floor: [2, 6],
thin_walls: {
__special__: 'thin_walls',
thin_walls_ns: [8, 4],
thin_walls_ew: [8, 5],
},
one_way_walls: {
__special__: 'thin_walls',
thin_walls_ns: [9, 4],
thin_walls_ew: [9, 5],
},
force_floor_n: {
__special__: 'scroll',
duration: 24,
cc2_duration: 8,
base: [0, 8],
scroll_region: [0, 1],
},
force_floor_e: {
__special__: 'scroll',
duration: 24,
cc2_duration: 8,
base: [3, 8],
scroll_region: [-1, 0],
},
force_floor_s: {
__special__: 'scroll',
duration: 24,
cc2_duration: 8,
base: [1, 9],
scroll_region: [0, -1],
},
force_floor_w: {
__special__: 'scroll',
duration: 24,
cc2_duration: 8,
base: [2, 9],
scroll_region: [1, 0],
},
water: {
__special__: 'animated',
duration: 36,
cc2_duration: 20,
all: [[4, 8], [5, 8], [6, 8], [7, 8]],
},
fire: {
__special__: 'animated',
duration: 36,
cc2_duration: 20,
all: [[4, 9], [5, 9], [6, 9], [7, 9]],
},
force_floor_all: {
__special__: 'animated',
duration: 24,
cc2_duration: 8,
all: [[0, 10], [1, 10], [2, 10], [3, 10], [4, 10], [5, 10], [6, 10], [7, 10]],
},
slime: {
__special__: 'animated',
duration: 60,
all: [[0, 11], [1, 11], [2, 11], [3, 11], [4, 11], [5, 11], [6, 11], [7, 11]],
},
turtle: {
// Turtles draw atop fake water, but don't act like water otherwise
__special__: 'overlay',
overlay: {
__special__: 'animated',
duration: 180,
positionally_hashed: true,
all: [[8, 8], [9, 8], [10, 8], [9, 8]],
},
base: 'water',
},
ice: [12, 8],
cracked_ice: [12, 9],
ice_se: [13, 8],
ice_sw: [14, 8],
ice_ne: [13, 9],
ice_nw: [14, 9],
dirt: [15, 8],
gravel: [15, 9],
green_floor: {
__special__: 'animated',
duration: 24,
cc2_duration: 16,
all: [[8, 10], [9, 10], [10, 10], [11, 10]],
},
green_wall: {
__special__: 'animated',
duration: 24,
cc2_duration: 16,
all: [[8, 11], [9, 11], [10, 11], [11, 11]],
},
purple_floor: {
__special__: 'animated',
duration: 24,
cc2_duration: 16,
all: [[12, 10], [13, 10], [14, 10], [15, 10]],
},
purple_wall: {
__special__: 'animated',
duration: 24,
cc2_duration: 16,
all: [[12, 11], [13, 11], [14, 11], [15, 11]],
},
// Cool movement tiles
railroad: {
__special__: 'railroad',
base: [15, 9],
railroad_ties: {
ne: [0, 12],
se: [1, 12],
sw: [2, 12],
nw: [3, 12],
ew: [4, 12],
ns: [5, 12],
},
railroad_active: {
ne: [0, 13],
se: [1, 13],
sw: [2, 13],
nw: [3, 13],
ew: [4, 13],
ns: [5, 13],
},
railroad_inactive: {
ne: [0, 14],
se: [1, 14],
sw: [2, 14],
nw: [3, 14],
ew: [4, 14],
ns: [5, 14],
},
railroad_switch: [6, 12],
},
swivel_floor: [7, 12],
swivel_se: [6, 13],
swivel_sw: [7, 13],
swivel_ne: [6, 14],
swivel_nw: [7, 14],
dash_floor: {
__special__: 'animated',
duration: 24,
all: [[0, 15], [1, 15], [2, 15], [3, 15], [4, 15], [5, 15], [6, 15], [7, 15]],
},
// Items
flippers: [0, 16],
fire_boots: [1, 16],
cleats: [2, 16],
suction_boots: [3, 16],
hiking_boots: [4, 16],
lightning_bolt: [5, 16],
speed_boots: [6, 16],
bribe: [7, 16],
railroad_sign: [0, 17],
hook: [1, 17],
foil: [2, 17],
xray_eye: [3, 17],
helmet: [4, 17],
skeleton_key: [0, 18],
ankh: [1, 18],
floor_ankh: [2, 18],
no_sign: [6, 18],
gift_bow: [7, 18],
score_10: [0, 19],
score_100: [1, 19],
score_1000: [2, 19],
score_2x: [3, 19],
score_5x: [4, 19],
stopwatch_bonus: [5, 19],
stopwatch_penalty: [6, 19],
stopwatch_toggle: [7, 19],
chip: {
__special__: 'animated',
duration: 24,
all: [[8, 16], [9, 16], [10, 16], [9, 16]],
},
chip_extra: {
__special__: 'perception',
modes: new Set(['palette', 'editor']),
hidden: {
__special__: 'animated',
duration: 24,
all: [[8, 16], [9, 16], [10, 16], [9, 16]],
},
revealed: [8, 19],
},
green_chip: {
__special__: 'animated',
duration: 24,
all: [[8, 17], [9, 17], [10, 17], [9, 17]],
},
bowling_ball: [9, 19],
rolling_ball: {
__special__: 'animated',
global: false,
duration: 1,
north: [[14, 16], [15, 16], [13, 17], [13, 17], [13, 17], [11, 16], [12, 16], [13, 16]],
east: [[11, 17], [12, 17], [13, 17], [13, 17], [13, 17], [14, 17], [15, 17], [13, 16]],
south: [[12, 16], [11, 16], [13, 17], [13, 17], [13, 17], [15, 16], [14, 16], [13, 16]],
west: [[15, 17], [14, 17], [13, 17], [13, 17], [13, 17], [12, 17], [11, 17], [13, 16]],
},
// LL bombs aren't animated
bomb: [11, 18],
green_bomb: [12, 18],
dynamite: [10, 19],
dynamite_lit: {
__special__: 'visual-state',
0: [11, 19],
1: [12, 19],
2: [13, 19],
3: [14, 19],
4: [15, 19],
},
// Doors and mechanisms
key_red: [0, 20],
key_blue: [0, 21],
key_yellow: [0, 22],
key_green: [0, 23],
door_red: [1, 20],
door_blue: [1, 21],
door_yellow: [1, 22],
door_green: [1, 23],
gate_red: [2, 20],
gate_blue: [2, 21],
gate_yellow: [2, 22],
gate_green: [2, 23],
teleport_red: {
__special__: 'wires',
base: [0, 2],
wired: {
__special__: 'visual-state',
active: {
__special__: 'animated',
duration: 20,
cc2_duration: 16,
all: [[4, 20], [5, 20], [6, 20], [7, 20]],
},
inactive: [9, 23],
},
},
teleport_blue: {
__special__: 'wires',
base: [0, 2],
wired: {
__special__: 'animated',
duration: 20,
cc2_duration: 16,
all: [[4, 21], [5, 21], [6, 21], [7, 21]],
},
},
teleport_yellow: {
__special__: 'animated',
duration: 20,
cc2_duration: 16,
all: [[4, 22], [5, 22], [6, 22], [7, 22]],
},
teleport_green: {
__special__: 'animated',
duration: 20,
cc2_duration: 16,
// Nice little touch: green teleporters aren't animated in sync
positionally_hashed: true,
all: [[4, 23], [5, 23], [6, 23], [7, 23]],
},
teleport_blue_exit: {
__special__: 'wires',
base: [0, 2],
wired: [8, 23],
},
transmogrifier: {
__special__: 'visual-state',
active: {
__special__: 'animated',
duration: 16,
all: [[8, 20], [9, 20], [10, 20], [11, 20]],
},
inactive: [10, 23],
},
turntable_cw: {
__special__: 'wires',
base: [0, 2],
wired: {
__special__: 'animated',
duration: 12,
cc2_duration: 16,
all: [[8, 22], [9, 22], [10, 22], [11, 22]],
}
},
turntable_ccw: {
__special__: 'wires',
base: [0, 2],
wired: {
__special__: 'animated',
duration: 12,
cc2_duration: 16,
all: [[8, 21], [9, 21], [10, 21], [11, 21]],
}
},
flame_jet_off: [12, 21],
flame_jet_on: {
__special__: 'animated',
duration: 18,
cc2_duration: 12,
all: [[13, 21], [14, 21], [15, 21]],
},
electrified_floor: {
__special__: 'visual-state',
active: {
__special__: 'animated',
duration: 18,
cc2_duration: 12,
all: [[13, 22], [14, 22], [15, 22]],
},
inactive: [12, 22],
},
// Buttons
button_blue: {
__special__: 'visual-state',
released: [0, 24],
pressed: [0, 25],
},
button_green: {
__special__: 'visual-state',
released: [1, 24],
pressed: [1, 25],
},
button_red: {
__special__: 'visual-state',
released: [2, 24],
pressed: [2, 25],
},
button_brown: {
__special__: 'visual-state',
released: [3, 24],
pressed: [3, 25],
},
button_pink: {
__special__: 'wires',
base: [0, 2],
wired: {
__special__: 'visual-state',
released: [4, 24],
pressed: [4, 25],
},
},
button_black: {
__special__: 'wires',
__special__: 'wires',
base: [0, 2],
wired: {
__special__: 'visual-state',
released: [5, 24],
pressed: [5, 25],
},
},
button_orange: {
__special__: 'visual-state',
released: [6, 24],
pressed: [6, 25],
},
button_gray: {
__special__: 'visual-state',
released: [7, 24],
pressed: [7, 25],
},
light_switch_off: {
__special__: 'wires',
base: [15, 25],
wired: [14, 24],
},
light_switch_on: {
__special__: 'wires',
base: [15, 25],
wired: [14, 25],
},
button_yellow: [15, 24],
cloner: [0, 26], // FIXME arrows at [0, 27]
trap: {
__special__: 'visual-state',
open: [1, 27],
closed: [1, 26],
},
// Wire and logic
'#unpowered': [2, 28],
'#powered': [2, 29],
'#wire-tunnel': [2, 30],
logic_gate: {
__special__: 'logic-gate',
counter_numbers: {
x: 7,
y: 1,
width: 0.75,
height: 1,
},
logic_gate_tiles: {
counter: [2, 31],
not: {
north: [3, 28],
east: [3, 29],
south: [3, 30],
west: [3, 31],
},
and: {
north: [4, 28],
east: [4, 29],
south: [4, 30],
west: [4, 31],
},
or: {
north: [5, 28],
east: [5, 29],
south: [5, 30],
west: [5, 31],
},
xor: {
north: [6, 28],
east: [6, 29],
south: [6, 30],
west: [6, 31],
},
nand: {
north: [7, 28],
east: [7, 29],
south: [7, 30],
west: [7, 31],
},
'latch-cw': {
north: [8, 28],
east: [8, 29],
south: [8, 30],
west: [8, 31],
},
'latch-ccw': {
north: [9, 28],
east: [9, 29],
south: [9, 30],
west: [9, 31],
},
diode: {
north: [10, 28],
east: [10, 29],
south: [10, 30],
west: [10, 31],
},
},
},
// ------------------------------------------------------------------------------------------------
// Right side: actors
player: {
__special__: 'visual-state',
normal: {
north: [16, 0],
east: [16, 1],
south: [16, 2],
west: [16, 3],
},
moving: {
__special__: 'animated',
global: false,
duration: 0.5,
cc2_duration: 1,
north: [[16, 0], [17, 0], [18, 0], [19, 0], [20, 0], [21, 0], [22, 0], [23, 0]],
east: [[16, 1], [17, 1], [18, 1], [19, 1], [20, 1], [21, 1], [22, 1], [23, 1]],
south: [[16, 2], [17, 2], [18, 2], [19, 2], [20, 2], [21, 2], [22, 2], [23, 2]],
west: [[16, 3], [17, 3], [18, 3], [19, 3], [20, 3], [21, 3], [22, 3], [23, 3]],
},
swimming: {
__special__: 'animated',
global: false,
duration: 1,
north: [[24, 0], [25, 0]],
east: [[24, 1], [25, 1]],
south: [[24, 2], [25, 2]],
west: [[24, 3], [25, 3]],
},
pushing: {
__special__: 'animated',
global: false,
duration: 0.5,
cc2_duration: 1,
north: [[26, 0], [27, 0], [28, 0], [27, 0]],
east: [[26, 1], [27, 1], [28, 1], [27, 1]],
south: [[26, 2], [27, 2], [28, 2], [27, 2]],
west: [[26, 3], [27, 3], [28, 3], [27, 3]],
},
blocked: {
north: [27, 0],
east: [27, 1],
south: [27, 2],
west: [27, 3],
},
skating: {
north: [29, 0],
east: [29, 1],
south: [29, 2],
west: [29, 3],
},
forced: 'skating',
burned: {
north: [30, 0],
east: [30, 1],
south: [30, 2],
west: [30, 3],
},
// These are frames from the splash/explosion animations
exploded: [17, 26],
failed: [17, 26],
drowned: [17, 27],
slimed: [17, 28],
fell: [17, 29],
exited: [31, 0],
},
bogus_player_win: {
__special__: 'overlay',
overlay: [16, 2],
base: 'exit',
},
bogus_player_swimming: {
north: [24, 0],
east: [24, 1],
south: [24, 2],
west: [24, 3],
},
bogus_player_drowned: {
__special__: 'overlay',
overlay: [17, 27], // splash
base: 'water',
},
bogus_player_burned_fire: {
__special__: 'overlay',
overlay: [17, 26], // explosion
base: 'fire',
},
bogus_player_burned: {
__special__: 'overlay',
overlay: [17, 26], // explosion
base: 'floor',
},
player2: {
__special__: 'visual-state',
normal: {
north: [16, 4],
east: [16, 5],
south: [16, 6],
west: [16, 7],
},
moving: {
__special__: 'animated',
global: false,
duration: 0.5,
cc2_duration: 1,
north: [[16, 4], [17, 4], [18, 4], [19, 4], [20, 4], [21, 4], [22, 4], [23, 4]],
east: [[16, 5], [17, 5], [18, 5], [19, 5], [20, 5], [21, 5], [22, 5], [23, 5]],
south: [[16, 6], [17, 6], [18, 6], [19, 6], [20, 6], [21, 6], [22, 6], [23, 6]],
west: [[16, 7], [17, 7], [18, 7], [19, 7], [20, 7], [21, 7], [22, 7], [23, 7]],
},
swimming: {
__special__: 'animated',
global: false,
duration: 1,
north: [[24, 4], [25, 4]],
east: [[24, 5], [25, 5]],
south: [[24, 6], [25, 6]],
west: [[24, 7], [25, 7]],
},
pushing: {
__special__: 'animated',
global: false,
duration: 0.5,
cc2_duration: 1,
north: [[26, 4], [27, 4], [28, 4], [27, 4]],
east: [[26, 5], [27, 5], [28, 5], [27, 5]],
south: [[26, 6], [27, 6], [28, 6], [27, 6]],
west: [[26, 7], [27, 7], [28, 7], [27, 7]],
},
blocked: {
north: [27, 4],
east: [27, 5],
south: [27, 6],
west: [27, 7],
},
skating: {
north: [29, 4],
east: [29, 5],
south: [29, 6],
west: [29, 7],
},
forced: 'skating',
burned: {
north: [30, 4],
east: [30, 5],
south: [30, 6],
west: [30, 7],
},
// These are frames from the splash/explosion animations
exploded: [17, 26],
failed: [17, 26],
drowned: [17, 27],
slimed: [17, 28],
fell: [17, 29],
exited: [31, 4],
},
tank_blue: {
__special__: 'animated',
duration: 20,
cc2_duration: 32,
north: [[16, 8], [17, 8]],
east: [[16, 9], [17, 9]],
south: [[16, 10], [17, 10]],
west: [[16, 11], [17, 11]],
},
tank_yellow: {
__special__: 'animated',
duration: 20,
cc2_duration: 32,
north: [[18, 8], [19, 8]],
east: [[18, 9], [19, 9]],
south: [[18, 10], [19, 10]],
west: [[18, 11], [19, 11]],
},
bug: {
__special__: 'animated',
global: false,
duration: 1,
north: [[20, 8], [21, 8], [22, 8], [23, 8]],
east: [[20, 9], [21, 9], [22, 9], [23, 9]],
south: [[20, 10], [21, 10], [22, 10], [23, 10]],
west: [[20, 11], [21, 11], [22, 11], [23, 11]],
},
paramecium: {
__special__: 'animated',
global: false,
duration: 1,
north: [[24, 8], [25, 8], [26, 8], [25, 8]],
east: [[24, 9], [25, 9], [26, 9], [25, 9]],
south: [[24, 10], [25, 10], [26, 10], [25, 10]],
west: [[24, 11], [25, 11], [26, 11], [25, 11]],
},
glider: {
__special__: 'animated',
duration: 10,
cc2_duration: 8,
north: [[27, 8], [28, 8]],
east: [[27, 9], [28, 9]],
south: [[27, 10], [28, 10]],
west: [[27, 11], [28, 11]],
},
ghost: {
north: [29, 8],
east: [29, 9],
south: [29, 10],
west: [29, 11],
},
blob: {
__special__: 'animated',
global: false,
duration: 1,
north: [[16, 12], [17, 12], [18, 12], [19, 12], [20, 12], [21, 12], [22, 12], [23, 12]],
east: [[16, 13], [17, 13], [18, 13], [19, 13], [20, 13], [21, 13], [22, 13], [23, 13]],
south: [[16, 14], [17, 14], [18, 14], [19, 14], [20, 14], [21, 14], [22, 14], [23, 14]],
west: [[16, 15], [17, 15], [18, 15], [19, 15], [20, 15], [21, 15], [22, 15], [23, 15]],
},
walker: {
__special__: 'animated',
global: false,
duration: 1,
north: [[24, 12], [25, 12], [26, 12], [27, 12]],
east: [[24, 13], [25, 13], [26, 13], [27, 13]],
// Same animations but played backwards
south: [[26, 12], [25, 12], [24, 12], [27, 12]],
west: [[26, 13], [25, 13], [24, 13], [27, 13]],
},
teeth: {
__special__: 'animated',
global: false,
duration: 1,
idle_frame_index: 1,
north: [[16, 16], [17, 16], [18, 16], [17, 16]],
east: [[16, 17], [17, 17], [18, 17], [17, 17]],
south: [[16, 18], [17, 18], [18, 18], [17, 18]],
west: [[16, 19], [17, 19], [18, 19], [17, 19]],
},
teeth_timid: {
__special__: 'animated',
global: false,
duration: 1,
idle_frame_index: 1,
north: [[19, 16], [20, 16], [21, 16], [20, 16]],
east: [[19, 17], [20, 17], [21, 17], [20, 17]],
south: [[19, 18], [20, 18], [21, 18], [20, 18]],
west: [[19, 19], [20, 19], [21, 19], [20, 19]],
},
// Blocks
dirt_block: {
__special__: 'perception',
modes: new Set(['editor', 'xray']),
hidden: [16, 20],
revealed: [16, 21],
},
ice_block: {
__special__: 'perception',
modes: new Set(['editor', 'xray']),
hidden: [17, 20],
revealed: [17, 21],
},
frame_block: {
__special__: 'arrows',
base: [18, 20],
arrows: [18, 21],
},
glass_block: {
__special__: 'encased_item',
base: [19, 21],
},
boulder: [20, 20],
circuit_block: {
__special__: 'wires',
base: [16, 22],
wired: [17, 22],
wired_cross: [18, 22],
},
sokoban_block: {
__special__: 'visual-state',
red: [26, 20],
blue: [26, 21],
yellow: [26, 22],
green: [26, 23],
},
sokoban_button: {
__special__: 'visual-state',
red_released: [28, 20],
blue_released: [28, 21],
yellow_released: [28, 22],
green_released: [28, 23],
red_pressed: [29, 20],
blue_pressed: [29, 21],
yellow_pressed: [29, 22],
green_pressed: [29, 23],
},
sokoban_wall: {
__special__: 'visual-state',
red: [30, 20],
blue: [30, 21],
yellow: [30, 22],
green: [30, 23],
},
sokoban_floor: {
__special__: 'visual-state',
red: [31, 20],
blue: [31, 21],
yellow: [31, 22],
green: [31, 23],
},
rover: {
__special__: 'rover',
direction: [26, 24],
inert: [16, 24],
teeth: {
__special__: 'animated',
duration: 16,
all: [[16, 24], [24, 24]],
},
// cw, slow
glider: {
__special__: 'animated',
duration: 32,
all: [[16, 24], [17, 24], [18, 24], [19, 24], [20, 24], [21, 24], [22, 24], [23, 24]],
},
// ccw, fast
bug: {
__special__: 'animated',
duration: 16,
all: [[23, 24], [22, 24], [21, 24], [20, 24], [19, 24], [18, 24], [17, 24], [16, 24]],
},
ball: {
__special__: 'animated',
duration: 16,
all: [[16, 24], [20, 24]],
},
teeth_timid: {
__special__: 'animated',
duration: 16,
all: [[16, 24], [25, 24]],
},
// ccw, slow
fireball: {
__special__: 'animated',
duration: 32,
all: [[23, 24], [22, 24], [21, 24], [20, 24], [19, 24], [18, 24], [17, 24], [16, 24]],
},
// cw, fast
paramecium: {
__special__: 'animated',
duration: 16,
all: [[16, 24], [17, 24], [18, 24], [19, 24], [20, 24], [21, 24], [22, 24], [23, 24]],
},
walker: {
__special__: 'animated',
duration: 16,
all: [[24, 24], [25, 24]],
},
},
ball: {
__special__: 'animated',
global: false,
duration: 0.5,
cc2_duration: 1,
idle_frame_index: 2,
// appropriately, this animation ping-pongs
all: [[27, 24], [28, 24], [29, 24], [30, 24], [31, 24], [30, 24], [29, 24], [28, 24]],
},
fireball: {
__special__: 'animated',
duration: 12,
cc2_duration: 4,
all: [[16, 25], [17, 25], [18, 25], [19, 25]],
},
floor_mimic: {
__special__: 'perception',
modes: new Set(['palette', 'editor', 'xray']),
hidden: [0, 2],
revealed: [31, 25],
},
// VFX
explosion: [[16, 26], [17, 26], [18, 26], [19, 26]],
explosion_nb: [[16, 26], [17, 26], [18, 26], [19, 26]],
splash: [[16, 27], [17, 27], [18, 27], [19, 27]],
splash_slime: [[16, 28], [17, 28], [18, 28], [19, 28]],
fall: [[16, 29], [17, 29], [18, 29], [19, 29]],
player1_exit: [[20, 28], [21, 28], [22, 28], [23, 28]],
player2_exit: [[20, 29], [21, 29], [22, 29], [23, 29]],
transmogrify_flash: [[24, 26], [25, 26], [26, 26], [27, 26], [28, 26], [29, 26], [30, 26], [31, 26]],
teleport_flash: [[24, 27], [25, 27], [26, 27], [27, 27]],
puff: [[24, 28], [25, 28], [26, 28], [27, 28]],
resurrection: [[23, 28], [22, 28], [21, 28], [20, 28]],
};
export const TILESET_LAYOUTS = {
// MS layout, either abbreviated or full
'tw-static': TILE_WORLD_TILESET_LAYOUT,
// "Large" (and dynamic, so not actually defined here) TW layout
'tw-animated': {
'#ident': 'tw-animated',
'#name': "Tile World (animated)",
'#supported-versions': new Set(['cc1']),
..._omit_custom_lexy_vfx,
},
cc2: CC2_TILESET_LAYOUT,
lexy: LL_TILESET_LAYOUT,
};
// Bundle of arguments for drawing a tile, containing some standard state about the game
export class DrawPacket {
constructor(perception = 'normal', hide_logic = false, clock = 0, update_progress = 0, update_rate = 3) {
this.perception = perception;
this.hide_logic = hide_logic && perception === 'normal';
this.use_cc2_anim_speed = false;
this.clock = clock;
this.update_progress = update_progress;
this.update_rate = update_rate;
this.show_facing = false;
// this.x
// this.y
}
// Draw a tile (or region) from the tileset. The caller is presumed to know where the tile
// goes, so the arguments here are only about how to find the tile on the sheet.
// tx, ty: Tile coordinates (from the tileset, measured in cells)
// mx, my, mw, mh: Optional mask to use for drawing a subset of a tile (or occasionally tiles)
// mdx, mdy: Where to draw the masked part; defaults to drawing aligned with the tile
blit(tx, ty, mx = 0, my = 0, mw = 1, mh = mw, mdx = mx, mdy = my) {}
// Same, but do not interpolate the position of an actor in motion; always draw it exactly in
// the cell it claims to be in
blit_aligned(tx, ty, mx = 0, my = 0, mw = 1, mh = mw, mdx = mx, mdy = my) {}
}
export class Tileset {
constructor(image, layout, size_x, size_y) {
// XXX curiously, i note that .image is never used within this class
this.image = image;
this.layout = layout;
this.size_x = size_x;
this.size_y = size_y;
}
draw(tile, packet) {
this.draw_type(tile.type.name, tile, packet);
}
// Draws a tile type, given by name. Passing in a tile is optional, but
// without it you'll get defaults.
draw_type(name, tile, packet) {
let drawspec = this.layout[name];
if (drawspec === undefined) {
// This is just missing
console.error(`Don't know how to draw tile type ${name}!`);
return;
}
this.draw_drawspec(drawspec, name, tile, packet);
if (packet.show_facing) {
this._draw_facing(name, tile, packet);
}
}
// Draw the facing direction of a tile (generally used by the editor)
_draw_facing(name, tile, packet) {
if (! (tile && tile.direction && TILE_TYPES[name].is_actor))
return;
// These are presumed to be half-size tiles
let drawspec = this.layout['#editor-arrows'];
if (! drawspec)
return;
let coords = drawspec[tile.direction];
let dirinfo = DIRECTIONS[tile.direction];
packet.blit(
...coords, 0, 0, 0.5, 0.5,
0.25 + dirinfo.movement[0] * 0.25,
0.25 + dirinfo.movement[1] * 0.25);
}
// Draw a "standard" drawspec, which is either:
// - a single tile: [x, y]
// - an animation: [[x0, y0], [x1, y1], ...]
// - a directional tile: { north: T, east: T, ... } where T is either of the above
_draw_standard(drawspec, name, tile, packet) {
// If we have an object, it must be a table of directions
let coords = drawspec;
if (!(coords instanceof Array)) {
coords = coords[(tile && tile.direction) ?? 'south'];
}
// Any animation not using the 'animated' special is a dedicated animation tile (like an
// explosion or splash) and just plays over the course of its lifetime
if (coords[0] instanceof Array) {
if (tile && tile.movement_speed) {
let p = tile.movement_progress(packet.update_progress, packet.update_rate);
coords = coords[Math.floor(p * coords.length)];
}
else {
coords = coords[0];
}
}
packet.blit(coords[0], coords[1]);
}
_draw_animated(drawspec, name, tile, packet) {
let frames;
if (drawspec.all) {
frames = drawspec.all;
}
else if (tile && tile.direction) {
frames = drawspec[tile.direction];
}
else {
frames = drawspec.south;
}
let is_global = drawspec.global ?? true;
let duration = drawspec.duration;
if (packet.use_cc2_anim_speed && drawspec.cc2_duration) {
duration = drawspec.cc2_duration;
}
let n;
if (is_global) {
// This tile animates on a global timer, looping every 'duration' frames
let p = packet.clock * 3 / duration;
// Lilypads bob at pseudo-random. CC2 has a much simpler approach to this, but it looks
// kind of bad with big patches of lilypads. It's 202x so let's use that CPU baby
if (drawspec.positionally_hashed) {
// This is the 32-bit FNV-1a hash algorithm, if you're curious
let h = 0x811c9dc5;
h = Math.imul(h ^ packet.x, 0x01000193);
h = Math.imul(h ^ packet.y, 0x01000193);
p += (h & 63) / 64;
}
n = Math.floor(p % 1 * frames.length);
}
else if (tile && tile.movement_speed) {
// This tile is in motion and its animation runs 'duration' times each move.
let p = tile.movement_progress(packet.update_progress, packet.update_rate);
duration = duration ?? 1;
if (duration < 1) {
// The 'duration' may be fractional; for example, the player's walk cycle is two
// steps, so its duration is 0.5 and each move plays half of the full animation.
// Consider: p, the current progress through the animation, is in [0, 1). To play
// the first half, we want [0, 0.5); to play the second half, we want [0.5, 1).
// Thus we add an integer in [0, 2) to offset us into which half to play, then
// divide by 2 to renormalize. Which half to use is determined by when the
// animation /started/, as measured in animation lengths.
let start_time = (packet.clock * 3 / tile.movement_speed) - p;
// Rounding smooths out float error (assuming the framerate never exceeds 1000)
let chunk_size = 1 / duration;
let segment = Math.floor(Math.round(start_time * 1000) / 1000 % chunk_size);
// It's possible for the segment to be negative here in very obscure cases (notably,
// if an actor in Lynx mode starts out on an open trap, it'll be artificially
// accelerated and will appear to have started animating before the first tic)
if (segment < 0) {
segment += chunk_size;
}
p = (p + segment) * duration;
}
else if (duration > 1) {
// Larger durations are much easier; just multiply and mod.
// (Note that large fractional durations like 2.5 will not work.)
p = p * duration % 1;
}
n = Math.floor(p * frames.length);
}
else {
// This is an actor that's not moving, so use the idle frame
n = drawspec.idle_frame_index ?? 0;
}
if (drawspec.triple) {
// Lynx-style big splashes and explosions
packet.blit(...frames[n], 0, 0, 3, 3, -1, -1);
}
else {
packet.blit(...frames[n]);
}
}
// Simple overlaying used for green/purple toggle tiles and doppelgangers. Draw the base (a
// type name or drawspec), then draw the overlay (either a type name or a regular draw spec).
_draw_overlay(drawspec, name, tile, packet) {
// TODO chance of infinite recursion here
if (typeof drawspec.base === 'string') {
this.draw_type(drawspec.base, tile, packet);
}
else {
this.draw_drawspec(drawspec.base, name, tile, packet);
}
if (typeof drawspec.overlay === 'string') {
this.draw_type(drawspec.overlay, tile, packet);
}
else {
this.draw_drawspec(drawspec.overlay, name, tile, packet);
}
}
// Scrolling region, used for force floors
_draw_scroll(drawspec, name, tile, packet) {
let [x, y] = drawspec.base;
let duration = drawspec.duration;
if (packet.use_cc2_anim_speed && drawspec.cc2_duration) {
duration = drawspec.cc2_duration;
}
x += drawspec.scroll_region[0] * (packet.clock * 3 / duration % 1);
y += drawspec.scroll_region[1] * (packet.clock * 3 / duration % 1);
// Round to pixels
x = Math.floor(x * this.size_x + 0.5) / this.size_x;
y = Math.floor(y * this.size_y + 0.5) / this.size_y;
packet.blit(x, y);
}
_draw_wires(drawspec, name, tile, packet) {
// This /should/ match CC2's draw order exactly, based on experimentation
let wire_radius = this.layout['#wire-width'] / 2;
// TODO circuit block with a lightning bolt is always powered
// TODO circuit block in motion doesn't inherit cell's power
if (tile && tile.wire_directions && ! packet.hide_logic) {
// Draw the base tile
packet.blit(drawspec.base[0], drawspec.base[1]);
let is_crossed = (tile.wire_directions === 0x0f && drawspec.wired_cross);
if (is_crossed && tile.powered_edges && tile.powered_edges !== 0x0f) {
// For crossed wires with different power, order matters; horizontal is on top
// TODO note that this enforces the CC2 wire order
let vert = tile.powered_edges & 0x05, horiz = tile.powered_edges & 0x0a;
this._draw_fourway_power_underlay(
vert ? this.layout['#powered'] : this.layout['#unpowered'], 0x05, packet);
this._draw_fourway_power_underlay(
horiz ? this.layout['#powered'] : this.layout['#unpowered'], 0x0a, packet);
}
else {
this._draw_fourway_tile_power(tile, tile.wire_directions, packet);
}
// Then draw the wired tile on top of it all
this.draw_drawspec(is_crossed ? drawspec.wired_cross : drawspec.wired, name, tile, packet);
}
else {
// There's no wiring here, so just draw the base and then draw the wired part on top
// as normal. If the wired part is optional (as is the case for flooring in the CC2
// tileset), draw the base as normal instead.
if (drawspec.is_wired_optional) {
this.draw_drawspec(drawspec.base, name, tile, packet);
}
else {
packet.blit(drawspec.base[0], drawspec.base[1]);
this.draw_drawspec(drawspec.wired, name, tile, packet);
}
}
// Wired tiles may also have tunnels, drawn on top of everything else
if (tile && tile.wire_tunnel_directions && ! packet.hide_logic) {
let tunnel_coords = this.layout['#wire-tunnel'];
let tunnel_width = 6/32;
let tunnel_length = 12/32;
let tunnel_offset = (1 - tunnel_width) / 2;
if (tile.wire_tunnel_directions & DIRECTIONS['north'].bit) {
packet.blit(tunnel_coords[0], tunnel_coords[1],
tunnel_offset, 0, tunnel_width, tunnel_length);
}
if (tile.wire_tunnel_directions & DIRECTIONS['south'].bit) {
packet.blit(tunnel_coords[0], tunnel_coords[1],
tunnel_offset, 1 - tunnel_length, tunnel_width, tunnel_length);
}
if (tile.wire_tunnel_directions & DIRECTIONS['west'].bit) {
packet.blit(tunnel_coords[0], tunnel_coords[1],
0, tunnel_offset, tunnel_length, tunnel_width);
}
if (tile.wire_tunnel_directions & DIRECTIONS['east'].bit) {
packet.blit(tunnel_coords[0], tunnel_coords[1],
1 - tunnel_length, tunnel_offset, tunnel_length, tunnel_width);
}
}
}
_draw_fourway_tile_power(tile, wires, packet) {
// Draw the unpowered tile underneath, if any edge is unpowered (and in fact if /none/ of it
// is powered then we're done here)
let powered = (tile.cell ? tile.powered_edges : 0) & wires;
if (! tile.cell || powered !== tile.wire_directions) {
this._draw_fourway_power_underlay(this.layout['#unpowered'], wires, packet);
if (! tile.cell || powered === 0)
return;
}
this._draw_fourway_power_underlay(this.layout['#powered'], powered, packet);
}
_draw_fourway_power_underlay(drawspec, bits, packet) {
// Draw the part as a single rectangle, initially just a small dot in the center, but
// extending out to any edge that has a bit present
let wire_radius = this.layout['#wire-width'] / 2;
let x0 = 0.5 - wire_radius;
let x1 = 0.5 + wire_radius;
let y0 = 0.5 - wire_radius;
let y1 = 0.5 + wire_radius;
if (bits & DIRECTIONS['north'].bit) {
y0 = 0;
}
if (bits & DIRECTIONS['east'].bit) {
x1 = 1;
}
if (bits & DIRECTIONS['south'].bit) {
y1 = 1;
}
if (bits & DIRECTIONS['west'].bit) {
x0 = 0;
}
packet.blit(drawspec[0], drawspec[1], x0, y0, x1 - x0, y1 - y0);
}
_draw_letter(drawspec, name, tile, packet) {
this.draw_drawspec(drawspec.base, name, tile, packet);
let glyph = tile ? tile.overlaid_glyph : "?";
if (drawspec.letter_glyphs[glyph]) {
let [x, y] = drawspec.letter_glyphs[glyph];
// XXX size is hardcoded here, but not below, meh
packet.blit(x, y, 0, 0, 0.5, 0.5, 0.25, 0.25);
}
else {
// Look for a range
let u = glyph.charCodeAt(0);
for (let rangedef of drawspec.letter_ranges) {
if (rangedef.range[0] <= u && u < rangedef.range[1]) {
let t = u - rangedef.range[0];
let x = rangedef.x0 + rangedef.w * (t % rangedef.columns);
let y = rangedef.y0 + rangedef.h * Math.floor(t / rangedef.columns);
packet.blit(x, y, 0, 0, rangedef.w, rangedef.h,
(1 - rangedef.w) / 2, (1 - rangedef.h) / 2);
break;
}
}
}
}
_draw_thin_walls(drawspec, name, tile, packet) {
let edges = tile ? tile.edges : 0x0f;
// TODO it would be /extremely/ cool to join corners diagonally, but i can't do that without
// access to the context, which defeats the whole purpose of this scheme. damn
if (edges & DIRECTIONS['north'].bit) {
packet.blit(...drawspec.thin_walls_ns, 0, 0, 1, 0.5);
}
if (edges & DIRECTIONS['east'].bit) {
packet.blit(...drawspec.thin_walls_ew, 0.5, 0, 0.5, 1);
}
if (edges & DIRECTIONS['south'].bit) {
packet.blit(...drawspec.thin_walls_ns, 0, 0.5, 1, 0.5);
}
if (edges & DIRECTIONS['west'].bit) {
packet.blit(...drawspec.thin_walls_ew, 0, 0, 0.5, 1);
}
}
_draw_thin_walls_cc1(drawspec, name, tile, packet) {
let edges = tile ? tile.edges : 0x0f;
// This is kinda best-effort since the tiles are not designed to combine
if ((edges & DIRECTIONS['south'].bit) && (edges & DIRECTIONS['east'].bit)) {
packet.blit(...drawspec.southeast);
}
else if (edges & DIRECTIONS['south'].bit) {
packet.blit(...drawspec.south);
}
else if (edges & DIRECTIONS['east'].bit) {
packet.blit(...drawspec.east);
}
if (edges & DIRECTIONS['north'].bit) {
packet.blit(...drawspec.north);
}
if (edges & DIRECTIONS['west'].bit) {
packet.blit(...drawspec.west);
}
}
_draw_bomb_fuse(drawspec, name, tile, packet) {
// Draw the base bomb
this.draw_drawspec(drawspec.bomb, name, tile, packet);
// The fuse is made up of four quarter-tiles and animates... um... at a rate. I cannot
// tell. I have spent over an hour poring over this and cannot find a consistent pattern.
// It might be random! I'm gonna say it loops every 0.3 seconds = 18 frames, so 4.5 frames
// per cel, I guess. No one will know. (But... I'll know.)
// Also it's drawn in the upper right, that's important.
let cel = Math.floor(packet.clock / 0.3 * 4) % 4;
packet.blit(...drawspec.fuse, 0.5 * (cel % 2), 0.5 * Math.floor(cel / 2), 0.5, 0.5, 0.5, 0);
}
// Frame blocks have an arrow overlay
_draw_arrows(drawspec, name, tile, packet) {
this.draw_drawspec(drawspec.base, name, tile, packet);
if (tile && tile.arrows) {
let [x, y] = drawspec.arrows;
let x0 = 0.25, y0 = 0.25, x1 = 0.75, y1 = 0.75;
if (tile.arrows.has('north')) {
y0 = 0;
}
if (tile.arrows.has('east')) {
x1 = 1;
}
if (tile.arrows.has('south')) {
y1 = 1;
}
if (tile.arrows.has('west')) {
x0 = 0;
}
packet.blit(x, y, x0, y0, x1 - x0, y1 - y0);
}
}
_draw_visual_state(drawspec, name, tile, packet) {
// Apply custom per-type visual states
// Note that these accept null, too, and return a default
let state = TILE_TYPES[name].visual_state(tile);
// If it's a string, that's an alias for another state
if (typeof drawspec[state] === 'string') {
state = drawspec[state];
}
if (! drawspec[state]) {
console.warn("No such state", state, "for tile", name, tile);
}
this.draw_drawspec(drawspec[state], name, tile, packet);
}
_draw_double_size_monster(drawspec, name, tile, packet) {
// CC2 and Lynx have double-size art for blobs and walkers that spans the tile they're
// moving from AND the tile they're moving into.
// CC2 also has an individual 1×1 static tile, used in all four directions.
if ((! tile || ! tile.movement_speed) && drawspec.base) {
this.draw_drawspec(drawspec.base, name, tile, packet);
return;
}
// CC2 only supports horizontal and vertical moves, not all four directions. The other two
// directions are the animations played in reverse. TW's large layout supports all four.
let direction = (tile ? tile.direction : null) ?? 'south';
let axis_cels = drawspec[direction];
let w = 1, h = 1, x = 0, y = 0, sx = 0, sy = 0, reverse = false;
if (direction === 'north') {
if (! axis_cels) {
axis_cels = drawspec.vertical;
reverse = true;
}
h = 2;
sy = 1;
}
else if (direction === 'south') {
if (! axis_cels) {
axis_cels = drawspec.vertical;
}
h = 2;
y = -1;
sy = -1;
}
else if (direction === 'west') {
if (! axis_cels) {
axis_cels = drawspec.horizontal;
reverse = true;
}
w = 2;
sx = 1;
}
else if (direction === 'east') {
if (! axis_cels) {
axis_cels = drawspec.horizontal;
}
w = 2;
x = -1;
sx = -1;
}
let index;
if (tile && tile.movement_speed) {
let p = tile.movement_progress(packet.update_progress, packet.update_rate);
index = Math.floor(p * axis_cels.length);
}
else {
index = drawspec.idle_frame_index ?? 0;
}
let cel = reverse ? axis_cels[axis_cels.length - index + 1] : axis_cels[index];
if (cel === null) {
// null means use the 1x1 "base" tile instead
packet.blit_aligned(...drawspec.base, 0, 0, 1, 1, sx, sy);
}
else {
packet.blit_aligned(...cel, 0, 0, w, h, x, y);
}
}
_draw_rover(drawspec, name, tile, packet) {
// Rovers draw fairly normally (with their visual_state giving the monster they're copying),
// but they also have an overlay indicating their direction
let state = tile ? tile.type.visual_state(tile) : 'inert';
this.draw_drawspec(drawspec[state], name, tile, packet);
if (! tile)
return;
// The direction overlay is one of four quarter-tiles, drawn about in the center of the
// rover but shifted an eighth of a tile in the direction in question
let overlay_position = this._rotate(tile.direction, 0.25, 0.125, 0.75, 0.625);
let index = {north: 0, east: 1, west: 2, south: 3}[tile.direction];
if (index === undefined)
return;
packet.blit(
...drawspec.direction,
0.5 * (index % 2), 0.5 * Math.floor(index / 2), 0.5, 0.5,
overlay_position[0], overlay_position[1]);
}
_draw_logic_gate(drawspec, name, tile, packet) {
// Layer 1: wiring state
// Always draw the unpowered wire base
let unpowered_coords = this.layout['#unpowered'];
let powered_coords = this.layout['#powered'];
packet.blit(...unpowered_coords);
if (tile && tile.cell) {
// What goes on top varies a bit...
let r = this.layout['#wire-width'] / 2;
if (tile.gate_type === 'not' || tile.gate_type === 'counter' || tile.gate_type === 'diode') {
this._draw_fourway_tile_power(tile, 0x0f, packet);
}
else {
if (tile.powered_edges & DIRECTIONS[tile.direction].bit) {
// Output (on top)
let [x0, y0, x1, y1] = this._rotate(tile.direction, 0.5 - r, 0, 0.5 + r, 0.5);
packet.blit(powered_coords[0], powered_coords[1], x0, y0, x1 - x0, y1 - y0);
}
if (tile.powered_edges & DIRECTIONS[DIRECTIONS[tile.direction].right].bit) {
// Right input, which includes the middle
// This actually covers the entire lower right corner, for bent inputs.
let [x0, y0, x1, y1] = this._rotate(tile.direction, 0.5 - r, 0.5 - r, 1, 1);
packet.blit(powered_coords[0], powered_coords[1], x0, y0, x1 - x0, y1 - y0);
}
if (tile.powered_edges & DIRECTIONS[DIRECTIONS[tile.direction].left].bit) {
// Left input, which does not include the middle
// This actually covers the entire lower left corner, for bent inputs.
let [x0, y0, x1, y1] = this._rotate(tile.direction, 0, 0.5 - r, 0.5 - r, 1);
packet.blit(powered_coords[0], powered_coords[1], x0, y0, x1 - x0, y1 - y0);
}
}
}
// Layer 2: the tile itself
this.draw_drawspec(drawspec.logic_gate_tiles[tile.gate_type], name, tile, packet);
// Layer 3: counter number
if (tile.gate_type === 'counter') {
let nums = drawspec.counter_numbers;
packet.blit(
nums.x, nums.y, tile.memory * nums.width, 0,
nums.width, nums.height, (1 - nums.width) / 2, (1 - nums.height) / 2);
}
}
_draw_railroad(drawspec, name, tile, packet) {
// All railroads have regular gravel underneath
// TODO would be nice to disambiguate since it's possible to have nothing visible
this.draw_drawspec(this.layout['gravel'], name, tile, packet);
// FIXME what do i draw if there's no tile?
let part_order = ['ne', 'se', 'sw', 'nw', 'ew', 'ns'];
let visible_parts = [];
let topmost_part = null;
for (let [i, part] of part_order.entries()) {
if (tile && (tile.tracks & (1 << i))) {
if (tile.track_switch === i) {
topmost_part = part;
}
visible_parts.push(part);
}
}
let has_switch = (tile && tile.track_switch !== null);
for (let part of visible_parts) {
this.draw_drawspec(drawspec.railroad_ties[part], name, tile, packet);
}
let tracks = has_switch ? drawspec.railroad_inactive : drawspec.railroad_active;
for (let part of visible_parts) {
if (part !== topmost_part) {
this.draw_drawspec(tracks[part], name, tile, packet);
}
}
if (topmost_part) {
this.draw_drawspec(drawspec.railroad_active[topmost_part], name, tile, packet);
}
if (has_switch) {
this.draw_drawspec(drawspec.railroad_switch, name, tile, packet);
}
}
_draw_encased_item(drawspec, name, tile, packet) {
//draw the encased item
if (tile !== null && tile.encased_item !== undefined && tile.encased_item !== null) {
this._draw_standard(this.layout[tile.encased_item], tile.encased_item, null, packet);
}
//then draw the glass block
this._draw_standard(drawspec.base, name, tile, packet);
}
draw_drawspec(drawspec, name, tile, packet) {
if (drawspec === null)
// This is explicitly never drawn (used for extra visual-only frills that don't exist in
// some tilesets)
return;
if (drawspec.__special__) {
if (drawspec.__special__ === 'animated') {
this._draw_animated(drawspec, name, tile, packet);
}
else if (drawspec.__special__ === 'overlay') {
this._draw_overlay(drawspec, name, tile, packet);
}
else if (drawspec.__special__ === 'scroll') {
this._draw_scroll(drawspec, name, tile, packet);
}
else if (drawspec.__special__ === 'wires') {
this._draw_wires(drawspec, name, tile, packet);
}
else if (drawspec.__special__ === 'letter') {
this._draw_letter(drawspec, name, tile, packet);
}
else if (drawspec.__special__ === 'thin_walls') {
this._draw_thin_walls(drawspec, name, tile, packet);
}
else if (drawspec.__special__ === 'thin_walls_cc1') {
this._draw_thin_walls_cc1(drawspec, name, tile, packet);
}
else if (drawspec.__special__ === 'bomb-fuse') {
this._draw_bomb_fuse(drawspec, name, tile, packet);
}
else if (drawspec.__special__ === 'arrows') {
this._draw_arrows(drawspec, name, tile, packet);
}
else if (drawspec.__special__ === 'visual-state') {
this._draw_visual_state(drawspec, name, tile, packet);
}
else if (drawspec.__special__ === 'double-size-monster') {
this._draw_double_size_monster(drawspec, name, tile, packet);
}
else if (drawspec.__special__ === 'rover') {
this._draw_rover(drawspec, name, tile, packet);
}
else if (drawspec.__special__ === 'perception') {
if (drawspec.modes.has(packet.perception)) {
this.draw_drawspec(drawspec.revealed, name, tile, packet);
}
else {
this.draw_drawspec(drawspec.hidden, name, tile, packet);
}
}
else if (drawspec.__special__ === 'logic-gate') {
if (packet.hide_logic) {
this.draw_type('floor', tile, packet);
}
else {
this._draw_logic_gate(drawspec, name, tile, packet);
}
}
else if (drawspec.__special__ === 'railroad') {
this._draw_railroad(drawspec, name, tile, packet);
}
else if (drawspec.__special__ === 'encased_item') {
this._draw_encased_item(drawspec, name, tile, packet);
}
else {
console.error(`No such special ${drawspec.__special__} for ${name}`);
}
return;
}
this._draw_standard(drawspec, name, tile, packet);
}
_rotate(direction, x0, y0, x1, y1) {
if (direction === 'east') {
return [1 - y1, x0, 1 - y0, x1];
}
else if (direction === 'south') {
return [1 - x1, 1 - y1, 1 - x0, 1 - y0];
}
else if (direction === 'west') {
return [y0, 1 - x1, y1, 1 - x0];
}
else {
return [x0, y0, x1, y1];
}
}
}
const TILE_WORLD_LARGE_TILE_ORDER = [
'floor',
'force_floor_n', 'force_floor_w', 'force_floor_s', 'force_floor_e', 'force_floor_all',
'ice', 'ice_se', 'ice_sw', 'ice_ne', 'ice_nw',
'gravel', 'dirt', 'water', 'fire', 'bomb', 'trap', 'thief_tools', 'hint',
'button_blue', 'button_green', 'button_red', 'button_brown',
'teleport_blue', 'wall',
'#thin_walls/north', '#thin_walls/west', '#thin_walls/south', '#thin_walls/east', '#thin_walls/southeast',
'fake_wall', 'green_floor', 'green_wall', 'popwall', 'cloner',
'door_red', 'door_blue', 'door_yellow', 'door_green',
'socket', 'exit', 'chip',
'key_red', 'key_blue', 'key_yellow', 'key_green',
'cleats', 'suction_boots', 'fire_boots', 'flippers',
// Bogus tiles
'bogus_exit_1', 'bogus_exit_2',
'bogus_player_burned_fire', 'bogus_player_burned', 'bogus_player_win', 'bogus_player_drowned',
'player1_swimming_n', 'player1_swimming_w', 'player1_swimming_s', 'player1_swimming_e',
// Actors
'#player1-moving', '#player1-pushing', 'dirt_block',
'tank_blue', 'ball', 'glider', 'fireball', 'bug', 'paramecium', 'teeth', 'blob', 'walker',
// Animations, which can be 3×3
'splash', 'explosion', 'disintegrate',
];
export function parse_tile_world_large_tileset(canvas) {
let ctx = canvas.getContext('2d');
let tw = null;
let layout = {
...TILESET_LAYOUTS['tw-animated'],
player: {
__special__: 'visual-state',
normal: 'moving',
blocked: 'pushing',
moving: {},
pushing: {},
swimming: 'moving',
// TODO in tile world, skating and forced both just slide the static sprite
skating: 'moving',
forced: 'moving',
exited: 'normal',
// FIXME really these should play to completion, like lynx...
drowned: null,
// slimed: n/a
burned: null,
exploded: null,
failed: null,
// fell: n/a
},
thin_walls: {
__special__: 'thin_walls_cc1',
},
};
let image_data = ctx.getImageData(0, 0, canvas.width, canvas.height);
let px = image_data.data; // 🙄
let uses_alpha;
let is_transparent;
if (px[7] === 0) {
uses_alpha = true;
// FIXME does tile world actually support this? and handle it like this? probably find out
is_transparent = i => {
return px[i + 3] === 0;
};
}
else {
uses_alpha = false;
let r = px[4];
let g = px[5];
let b = px[6];
is_transparent = i => {
return px[i] === r && px[i + 1] === g && px[i + 2] === b;
};
}
// Scan out the rows first so we know them ahead of time
let th = null;
let prev_y = null;
let row_heights = {}; // first row => height in tiles
for (let y = 0; y < canvas.height; y++) {
let i = y * canvas.width * 4;
if (is_transparent(i))
continue;
if (prev_y !== null) {
let row_height = y - prev_y - 1; // fencepost
if (th === null) {
th = row_height;
if (th < 4)
throw new Error(`Bad tile height ${th}, this may not be a TW large tileset`);
}
if (row_height % th !== 0) {
console.warn("Tile height seems to be", th, "but row between", prev_y, "and", y,
"is not an integral multiple");
}
row_heights[prev_y] = row_height / th;
}
prev_y = y;
}
let i = 0;
let t = 0;
for (let y = 0; y < canvas.height; y++) {
let is_divider_row = false;
let prev_x = null;
for (let x = 0; x < canvas.width; x++) {
let trans = is_transparent(i);
if (trans && ! uses_alpha) {
px[i] = px[i + 1] = px[i + 2] = px[i + 3] = 0;
}
i += 4;
if (x === 0) {
is_divider_row = ! trans;
prev_x = x;
continue;
}
if (! (is_divider_row && ! trans))
continue;
if (t >= TILE_WORLD_LARGE_TILE_ORDER.length)
continue;
// This is an opaque pixel in a divider row, which marks the end of a tile
if (tw === null) {
tw = x - prev_x;
if (tw < 4)
throw new Error(`Bad tile width ${tw}, this may not be a TW large tileset`);
}
let name = TILE_WORLD_LARGE_TILE_ORDER[t];
let spec;
let num_columns = (x - prev_x) / tw;
let num_rows = row_heights[y];
if (num_rows === 0 || num_columns === 0)
throw new Error(`Bad row/column count (${num_rows}, ${num_columns}) at ${x}, ${y}`);
let x0 = (prev_x + 1) / tw;
let y0 = (y + 1) / th;
if (num_rows === 1 && num_columns === 1) {
spec = [x0, y0];
}
else if (59 <= t && t <= 71) {
// Actors have special layouts, one of several options
if (num_rows === 1 && num_columns === 2) {
// NS, EW
spec = {
north: [x0, y0],
south: [x0, y0],
east: [x0 + 1, y0],
west: [x0 + 1, y0],
};
}
else if (num_rows === 1 && num_columns === 4) {
// N, W, S, E
spec = {
north: [x0, y0],
west: [x0 + 1, y0],
south: [x0 + 2, y0],
east: [x0 + 3, y0],
};
}
else if (num_rows === 2 && num_columns === 1) {
// NS; EW
spec = {
north: [x0, y0],
south: [x0, y0],
east: [x0, y0 + 1],
west: [x0, y0 + 1],
};
}
else if (num_rows === 2 && num_columns === 2) {
// N, W; S, E
spec = {
north: [x0, y0],
west: [x0 + 1, y0],
south: [x0, y0 + 1],
east: [x0 + 1, y0 + 1],
};
}
else if (num_rows === 2 && num_columns === 8) {
// N N N N, W W W W; S S S S, E E E E
spec = {
__special__: 'animated',
// FIXME when global?
global: false,
duration: 1,
idle_frame_index: 1,
north: [[x0, y0], [x0 + 1, y0], [x0 + 2, y0], [x0 + 3, y0]],
west: [[x0 + 4, y0], [x0 + 5, y0], [x0 + 6, y0], [x0 + 7, y0]],
south: [[x0, y0 + 1], [x0 + 1, y0 + 1], [x0 + 2, y0 + 1], [x0 + 3, y0 + 1]],
east: [[x0 + 4, y0 + 1], [x0 + 5, y0 + 1], [x0 + 6, y0 + 1], [x0 + 7, y0 + 1]],
};
}
else if (num_rows === 2 && num_columns === 16) {
// Double-tile arranged as:
// NNNN SSSS WWWWWWWW
// NNNN SSSS EEEEEEEE
spec = {
__special__: 'double-size-monster',
idle_frame_index: 3,
north: [[x0, y0], [x0 + 1, y0], [x0 + 2, y0], [x0 + 3, y0]],
south: [[x0 + 4, y0], [x0 + 5, y0], [x0 + 6, y0], [x0 + 7, y0]],
west: [[x0 + 8, y0], [x0 + 10, y0], [x0 + 12, y0], [x0 + 14, y0]],
east: [[x0 + 8, y0 + 1], [x0 + 10, y0 + 1], [x0 + 12, y0 + 1], [x0 + 14, y0 + 1]],
};
}
else {
throw new Error(`Invalid layout for ${name}: ${num_columns} tiles wide by ${num_rows} tiles tall`);
}
}
else if (t >= 72) {
// One of the explosion animations; should be a single row, must be 6 or 12 frames,
// BUT is allowed to be triple size
spec = {
__special__: 'animated',
global: false,
duration: 1,
all: [],
};
for (let f = 0; f < num_columns; f += num_rows) {
spec['all'].push([x0 + f, y0]);
}
if (num_rows === 3) {
spec.triple = true;
}
}
else {
// Everyone else is a static tile, automatically animated
// TODO enforce only one row
spec = {
__special__: 'animated',
duration: 3 * num_columns, // one tic per frame
all: [],
};
for (let f = 0; f < num_columns; f++) {
spec['all'].push([x0 + f, y0]);
}
}
// Handle some special specs
if (name === '#player1-moving') {
layout['player']['moving'] = spec;
}
else if (name === '#player1-pushing') {
layout['player']['pushing'] = spec;
}
else if (name.startsWith('#thin_walls/')) {
let direction = name.match(/\/(\w+)$/)[1];
layout['thin_walls'][direction] = spec;
// Erase the floor
for (let f = 0; f < num_columns; f += 1) {
erase_thin_wall_floor(
image_data, prev_x + 1 + f * tw, y + 1,
layout['floor'][0] * tw, layout['floor'][1] * th,
tw, th);
}
}
else {
layout[name] = spec;
if (name === 'floor') {
layout['wall_appearing'] = spec;
layout['wall_invisible'] = spec;
}
else if (name === 'wall') {
layout['wall_invisible_revealed'] = spec;
}
else if (name === 'fake_wall') {
layout['fake_floor'] = spec;
}
else if (name === 'splash' || name === 'explosion') {
let n = Math.floor(0.25 * spec['all'].length);
let cel = spec['all'][n];
if (spec.triple) {
cel = [cel[0] + 1, cel[1] + 1];
}
// TODO remove these sometime
if (name === 'splash') {
layout['player']['drowned'] = cel;
}
else if (name === 'explosion') {
layout['player']['burned'] = cel;
layout['player']['exploded'] = cel;
layout['player']['failed'] = cel;
}
}
}
prev_x = x;
t += 1;
}
}
ctx.putImageData(image_data, 0, 0);
return new Tileset(canvas, layout, tw, th);
}
// MSCC repeats all the actor columns three times: once for an actor on top of normal floor (which
// we don't use because we expect everything transparent), once for the actor on a solid background,
// and once for a mask used to cut it out. Combine (3) with (2) and write it atop (1).
function apply_mscc_mask(canvas) {
let ctx = canvas.getContext('2d');
let image_data = ctx.getImageData(0, 0, canvas.width, canvas.height);
let px = image_data.data;
let tw = canvas.width / 13;
let dest_x0 = tw * 4;
let src_x0 = tw * 7;
let mask_x0 = tw * 10;
for (let y = 0; y < canvas.height; y++) {
let dest_i = (y * canvas.width + dest_x0) * 4;
let src_i = (y * canvas.width + src_x0) * 4;
let mask_i = (y * canvas.width + mask_x0) * 4;
for (let dx = 0; dx < tw * 3; dx++) {
px[dest_i + 0] = px[src_i + 0];
px[dest_i + 1] = px[src_i + 1];
px[dest_i + 2] = px[src_i + 2];
px[dest_i + 3] = px[mask_i];
dest_i += 4;
src_i += 4;
mask_i += 4;
}
}
// Resizing the canvas clears it, so do that first
canvas.width = canvas.height / 16 * 7;
ctx.putImageData(image_data, 0, 0);
}
// CC1 considered thin walls to be a floor tile, but CC2 makes them a transparent overlay. LL is
// designed to work like CC2, so to make a CC1 tileset work, try erasing the floor out from under a
// thin wall. This is extremely best-effort; it won't work very well if the wall has a shadow or
// happens to share some pixels with the floor below.
function erase_thin_wall_floor(image_data, wall_x0, wall_y0, floor_x0, floor_y0, tile_width, tile_height) {
let px = image_data.data;
for (let dy = 0; dy < tile_height; dy++) {
let wall_i = ((wall_y0 + dy) * image_data.width + wall_x0) * 4;
let floor_i = ((floor_y0 + dy) * image_data.width + floor_x0) * 4;
for (let dx = 0; dx < tile_width; dx++) {
if (px[wall_i + 3] > 0 && px[wall_i] === px[floor_i] &&
px[wall_i + 1] === px[floor_i + 1] && px[wall_i + 2] === px[floor_i + 2])
{
px[wall_i + 3] = 0;
}
wall_i += 4;
floor_i += 4;
}
}
}
function erase_mscc_thin_wall_floors(image_data, layout, tw, th) {
let floor_spec = layout['floor'];
let floor_x = floor_spec[0] * tw;
let floor_y = floor_spec[1] * th;
for (let direction of ['north', 'south', 'east', 'west', 'southeast']) {
let spec = layout['thin_walls'][direction];
erase_thin_wall_floor(image_data, spec[0] * tw, spec[1] * th, floor_x, floor_y, tw, th);
}
}
function erase_tileset_background(image_data, layout) {
let trans = layout['#transparent-color'];
if (! trans)
return;
let px = image_data.data;
if (trans.length === 2) {
// Read the background color from a pixel
let i = trans[0] + trans[1] * image_data.width;
if (px[i + 3] === 0) {
// Background is already transparent!
return;
}
trans = [px[i], px[i + 1], px[i + 2], px[i + 3]];
}
for (let i = 0; i < image_data.width * image_data.height * 4; i += 4) {
if (px[i] === trans[0] && px[i + 1] === trans[1] &&
px[i + 2] === trans[2] && px[i + 3] === trans[3])
{
px[i] = 0;
px[i + 1] = 0;
px[i + 2] = 0;
px[i + 3] = 0;
}
}
return true;
}
export function infer_tileset_from_image(img, make_canvas) {
// 99% of the time, we'll need a canvas anyway, so might as well create it now
let canvas = make_canvas(img.naturalWidth, img.naturalHeight);
let ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
// Determine the layout from the image dimensions. Try the usual suspects first
let aspect_ratio = img.naturalWidth / img.naturalHeight;
// Special case: the "full" MS layout, which MSCC uses internally; it's the same layout as TW's
// abbreviated one, but it needs its "mask" columns converted to a regular alpha channel
if (aspect_ratio === 13/16) {
apply_mscc_mask(canvas);
aspect_ratio = 7/16;
}
for (let layout of Object.values(TILESET_LAYOUTS)) {
if (! ('#dimensions' in layout))
continue;
let [w, h] = layout['#dimensions'];
// XXX this assumes square tiles, but i have written mountains of code that doesn't!
if (w / h === aspect_ratio) {
let image_data = ctx.getImageData(0, 0, canvas.width, canvas.height);
let did_anything = erase_tileset_background(image_data.data, layout);
let tw = Math.floor(canvas.width / w);
let th = Math.floor(canvas.height / h);
if (layout['#ident'] === 'tw-static') {
did_anything = true;
erase_mscc_thin_wall_floors(image_data, layout, tw, th);
}
if (did_anything) {
ctx.putImageData(image_data, 0, 0);
}
return new Tileset(canvas, layout, tw, th);
}
}
// Anything else could be Tile World's "large" layout, which has no fixed dimensions
return parse_tile_world_large_tileset(canvas);
}