When placed atop an item, you must have that item to enter the tile. When you do, pay the item and destroy the item lock. Also can be placed on top of a bonus, and you must pay that amount of bonus to enter.
1660 lines
54 KiB
JavaScript
1660 lines
54 KiB
JavaScript
import { DIRECTIONS } from './defs.js';
|
|
import TILE_TYPES from './tiletypes.js';
|
|
|
|
const _omit_custom_lexy_vfx = {
|
|
teleport_flash: null,
|
|
transmogrify_flash: null,
|
|
puff: null,
|
|
};
|
|
|
|
// TODO move the remaining stuff (arrows, overlay i think, probably force floor thing) into specials
|
|
// TODO more explicitly define animations, give them a speed! maybe fold directions into it
|
|
// TODO relatedly, the push animations are sometimes glitchy depending on when you start?
|
|
// TODO animate swimming player always
|
|
// TODO life might be easier if i used the lynx-style loop with cooldown at the end
|
|
// TODO define a draw state object to pass into here; need it for making turtles work right, fixing
|
|
// blur with cc2 blobs/walkers, also makes a lot of signatures cleaner (make sure not slower)
|
|
// TODO monsters should only animate while moving? (not actually how cc2 works...)
|
|
export const CC2_TILESET_LAYOUT = {
|
|
'#ident': 'cc2',
|
|
'#name': "Chip's Challenge 2",
|
|
'#dimensions': [16, 32],
|
|
'#transparent-color': [0x52, 0xce, 0x6b, 0xff],
|
|
'#supported-versions': new Set(['cc1', 'cc2']),
|
|
'#wire-width': 1/16,
|
|
|
|
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],
|
|
},
|
|
// FIXME this shouldn't be visible with seeing eye (or should it not spawn at all?)
|
|
wall_invisible_revealed: [1, 2],
|
|
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: [
|
|
[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]],
|
|
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: [[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: {
|
|
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: {
|
|
north: [[0, 8], [1, 8]],
|
|
east: [[2, 8], [3, 8]],
|
|
south: [[4, 8], [5, 8]],
|
|
west: [[6, 8], [7, 8]],
|
|
},
|
|
glider: {
|
|
north: [[8, 8], [9, 8]],
|
|
east: [[10, 8], [11, 8]],
|
|
south: [[12, 8], [13, 8]],
|
|
west: [[14, 8], [15, 8]],
|
|
},
|
|
|
|
green_floor: [[0, 9], [1, 9], [2, 9], [3, 9]],
|
|
purple_floor: [[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 animation is REALLY FAST, runs roughly twice per move
|
|
fireball: [
|
|
[12, 9], [13, 9], [14, 9], [15, 9],
|
|
[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: [[4, 10], [5, 10], [6, 10], [7, 10]],
|
|
},
|
|
popwall: [8, 10],
|
|
popwall2: [8, 10],
|
|
gravel: [9, 10],
|
|
ball: [
|
|
// appropriately, this animation ping-pongs
|
|
[10, 10], [11, 10], [12, 10], [13, 10],
|
|
[14, 10], [13, 10], [12, 10], [11, 10],
|
|
// FIXME the ball bounces so it specifically needs to play its animation every move; this
|
|
// defeats the ½x slowdown. it's dumb and means this anim as written doesn't match cc2
|
|
[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,
|
|
},
|
|
|
|
// TODO should explicitly set the non-moving tile, so we can have the walk tile start with
|
|
// immediate movement?
|
|
// TODO this shouldn't run at half speed, it's already designed to be one step, and when teeth
|
|
// move at half speed it looks clumsy
|
|
teeth: {
|
|
// NOTE: CC2 inexplicably dropped north teeth and just uses the south sprites instead
|
|
north: [[1, 11], [0, 11], [1, 11], [2, 11]],
|
|
east: [[4, 11], [3, 11], [4, 11], [5, 11]],
|
|
south: [[1, 11], [0, 11], [1, 11], [2, 11]],
|
|
west: [[7, 11], [6, 11], [7, 11], [8, 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: {
|
|
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: [13, 12], // TODO also 14 + 15, bobbing pseudorandomly
|
|
base: 'water',
|
|
},
|
|
|
|
walker: {
|
|
__special__: 'double-size-monster',
|
|
base: [0, 13],
|
|
vertical: [[1, 13], [2, 13], [3, 13], [4, 13], [5, 13], [6, 13], [7, 13]],
|
|
horizontal: [[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: [[1, 15], [2, 15], [3, 15], [4, 15], [5, 15], [6, 15], [7, 15]],
|
|
horizontal: [[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: {
|
|
// 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: [[6, 17], [7, 17]],
|
|
tank_yellow: {
|
|
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: [[0, 18], [8, 18]],
|
|
// cw, slow
|
|
glider: [[0, 18], [1, 18], [2, 18], [3, 18], [4, 18], [5, 18], [6, 18], [7, 18]],
|
|
// ccw, fast
|
|
bug: [
|
|
[7, 18], [6, 18], [5, 18], [4, 18], [3, 18], [2, 18], [1, 18], [0, 18],
|
|
[7, 18], [6, 18], [5, 18], [4, 18], [3, 18], [2, 18], [1, 18], [0, 18],
|
|
],
|
|
ball: [[0, 18], [4, 18]],
|
|
teeth_timid: [[0, 18], [9, 18]],
|
|
// ccw, slow
|
|
fireball: [[7, 18], [6, 18], [5, 18], [4, 18], [3, 18], [2, 18], [1, 18], [0, 18]],
|
|
// cw, fast
|
|
paramecium: [
|
|
[0, 18], [1, 18], [2, 18], [3, 18], [4, 18], [5, 18], [6, 18], [7, 18],
|
|
[0, 18], [1, 18], [2, 18], [3, 18], [4, 18], [5, 18], [6, 18], [7, 18],
|
|
],
|
|
walker: [[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',
|
|
base: [0, 19],
|
|
scroll_region: [0, 1],
|
|
},
|
|
force_floor_e: {
|
|
__special__: 'scroll',
|
|
base: [3, 19],
|
|
scroll_region: [-1, 0],
|
|
},
|
|
force_floor_s: {
|
|
__special__: 'scroll',
|
|
base: [1, 20],
|
|
scroll_region: [0, -1],
|
|
},
|
|
force_floor_w: {
|
|
__special__: 'scroll',
|
|
base: [2, 20],
|
|
scroll_region: [1, 0],
|
|
},
|
|
teleport_green: [[4, 19], [5, 19], [6, 19], [7, 19]],
|
|
teleport_yellow: [[8, 19], [9, 19], [10, 19], [11, 19]],
|
|
transmogrifier: {
|
|
__special__: 'visual-state',
|
|
active: [[12, 19], [13, 19], [14, 19], [15, 19]],
|
|
inactive: [12, 19],
|
|
},
|
|
teleport_red: {
|
|
__special__: 'wires',
|
|
base: [0, 2],
|
|
wired: {
|
|
__special__: 'visual-state',
|
|
active: [[4, 20], [5, 20], [6, 20], [7, 20]],
|
|
inactive: [4, 20],
|
|
},
|
|
},
|
|
slime: [[8, 20], [9, 20], [10, 20], [11, 20], [12, 20], [13, 20], [14, 20], [15, 20]],
|
|
|
|
force_floor_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: {
|
|
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: {
|
|
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 on ice
|
|
skating: [[0, 22], [8, 22], [0, 23], [8, 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], [1, 24]],
|
|
east: [[2, 24], [3, 24]],
|
|
south: [[4, 24], [5, 24]],
|
|
west: [[6, 24], [7, 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: [
|
|
[12, 24],
|
|
[13, 24],
|
|
[14, 24],
|
|
[15, 24],
|
|
],
|
|
|
|
logic_gate: {
|
|
__special__: 'logic-gate',
|
|
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],
|
|
},
|
|
diode: {
|
|
north: [0, 41],
|
|
east: [1, 41],
|
|
south: [2, 41],
|
|
west: [3, 41],
|
|
},
|
|
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: 'pushing',
|
|
moving: {
|
|
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: {
|
|
north: [8, 29],
|
|
east: [9, 29],
|
|
south: [10, 29],
|
|
west: [11, 29],
|
|
},
|
|
swimming: {
|
|
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
|
|
skating: [[0, 27], [8, 27], [0, 28], [8, 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: [
|
|
[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,
|
|
};
|
|
|
|
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_wall: [1, 14],
|
|
fake_floor: [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_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',
|
|
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 = Object.assign({}, CC2_TILESET_LAYOUT, {
|
|
'#ident': 'lexy',
|
|
'#name': "Lexy's Labyrinth",
|
|
// TODO dimensions, when this is stable?? might one day rearrange, leave some extra space
|
|
'#supported-versions': new Set(['cc1', 'cc2', 'll']),
|
|
|
|
// Completed teeth sprites
|
|
teeth: Object.assign({}, CC2_TILESET_LAYOUT.teeth, {
|
|
north: [[1, 32], [0, 32], [1, 32], [2, 32]],
|
|
}),
|
|
teeth_timid: {
|
|
north: [[7, 32], [6, 32], [7, 32], [8, 32]],
|
|
east: [[4, 32], [2, 17], [4, 32], [3, 17]],
|
|
south: [[3, 32], [0, 17], [3, 32], [1, 17]],
|
|
west: [[5, 32], [4, 17], [5, 32], [5, 17]],
|
|
},
|
|
|
|
// Extra player sprites
|
|
player: Object.assign({}, CC2_TILESET_LAYOUT.player, {
|
|
__special__: 'visual-state',
|
|
pushing: {
|
|
north: [[8, 24], [0, 34], [8, 24], [1, 34]],
|
|
east: [[9, 24], [2, 34], [9, 24], [3, 34]],
|
|
south: [[10, 24], [4, 34], [10, 24], [5, 34]],
|
|
west: [[11, 24], [6, 34], [11, 24], [7, 34]],
|
|
},
|
|
skating: {
|
|
north: [0, 33],
|
|
east: [1, 33],
|
|
south: [2, 33],
|
|
west: [3, 33],
|
|
},
|
|
forced: 'skating',
|
|
exited: [14, 32],
|
|
burned: {
|
|
north: [4, 33],
|
|
east: [5, 33],
|
|
south: [6, 33],
|
|
west: [7, 33],
|
|
},
|
|
slimed: [1, 38],
|
|
}),
|
|
player2: Object.assign({}, CC2_TILESET_LAYOUT.player2, {
|
|
__special__: 'visual-state',
|
|
pushing: {
|
|
north: [[8, 29], [8, 34], [8, 29], [9, 34]],
|
|
east: [[9, 29], [10, 34], [9, 29], [11, 34]],
|
|
south: [[10, 29], [12, 34], [10, 29], [13, 34]],
|
|
west: [[11, 29], [14, 34], [11, 29], [15, 34]],
|
|
},
|
|
skating: {
|
|
north: [8, 33],
|
|
east: [9, 33],
|
|
south: [10, 33],
|
|
west: [11, 33],
|
|
},
|
|
forced: 'skating',
|
|
exited: [15, 32],
|
|
burned: {
|
|
north: [12, 33],
|
|
east: [13, 33],
|
|
south: [14, 33],
|
|
west: [15, 33],
|
|
},
|
|
slimed: [1, 38],
|
|
}),
|
|
bogus_player_burned_fire: {
|
|
__special__: 'overlay',
|
|
overlay: [6, 33],
|
|
base: 'fire',
|
|
},
|
|
bogus_player_burned: {
|
|
__special__: 'overlay',
|
|
overlay: [6, 33],
|
|
base: 'floor',
|
|
},
|
|
|
|
// Custom tiles
|
|
popwall2: [9, 32],
|
|
gift_bow: [10, 32],
|
|
circuit_block: {
|
|
__special__: 'wires',
|
|
base: [13, 32],
|
|
wired: [11, 32],
|
|
wired_cross: [12, 32],
|
|
},
|
|
|
|
// Blob and walker in all four directions
|
|
blob: {
|
|
north: [[0, 35], [1, 35], [2, 35], [3, 35], [4, 35], [5, 35], [6, 35], [7, 35]],
|
|
east: [[8, 35], [9, 35], [10, 35], [11, 35], [12, 35], [13, 35], [14, 35], [15, 35]],
|
|
south: [[0, 36], [1, 36], [2, 36], [3, 36], [4, 36], [5, 36], [6, 36], [7, 36]],
|
|
west: [[8, 36], [9, 36], [10, 36], [11, 36], [12, 36], [13, 36], [14, 36], [15, 36]],
|
|
},
|
|
walker: {
|
|
north: [[0, 37], [1, 37], [2, 37], [3, 37]],
|
|
east: [[4, 37], [5, 37], [6, 37], [7, 37]],
|
|
// Same animations but played backwards
|
|
south: [[2, 37], [1, 37], [0, 37], [3, 37]],
|
|
west: [[6, 37], [5, 37], [4, 37], [7, 37]],
|
|
},
|
|
// Pressed buttons
|
|
button_blue: {
|
|
__special__: 'visual-state',
|
|
released: [8, 6],
|
|
pressed: [8, 37],
|
|
},
|
|
button_green: {
|
|
__special__: 'visual-state',
|
|
released: [9, 6],
|
|
pressed: [9, 37],
|
|
},
|
|
button_red: {
|
|
__special__: 'visual-state',
|
|
released: [10, 6],
|
|
pressed: [10, 37],
|
|
},
|
|
button_brown: {
|
|
__special__: 'visual-state',
|
|
released: [11, 6],
|
|
pressed: [11, 37],
|
|
},
|
|
button_pink: {
|
|
__special__: 'wires',
|
|
base: [0, 2],
|
|
wired: {
|
|
__special__: 'visual-state',
|
|
released: [12, 6],
|
|
pressed: [12, 37],
|
|
},
|
|
},
|
|
button_black: {
|
|
__special__: 'wires',
|
|
__special__: 'wires',
|
|
base: [0, 2],
|
|
wired: {
|
|
__special__: 'visual-state',
|
|
released: [13, 6],
|
|
pressed: [13, 37],
|
|
},
|
|
},
|
|
button_orange: {
|
|
__special__: 'visual-state',
|
|
released: [14, 6],
|
|
pressed: [14, 37],
|
|
},
|
|
button_gray: {
|
|
__special__: 'visual-state',
|
|
released: [11, 9],
|
|
pressed: [15, 37],
|
|
},
|
|
|
|
// Custom VFX
|
|
splash_slime: [[0, 38], [1, 38], [2, 38], [3, 38]],
|
|
teleport_flash: [[4, 38], [5, 38], [6, 38], [7, 38]],
|
|
chip_extra: {
|
|
__special__: 'perception',
|
|
modes: new Set(['palette', 'editor']),
|
|
hidden: [[11, 3], [0, 39], [1, 39], [0, 39]],
|
|
revealed: [10, 3],
|
|
},
|
|
chip: [[11, 3], [0, 39], [1, 39], [0, 39]],
|
|
green_chip: [[9, 3], [2, 39], [3, 39], [2, 39]],
|
|
player1_exit: [[8, 38], [9, 38], [10, 38], [11, 38]],
|
|
player2_exit: [[12, 38], [13, 38], [14, 38], [15, 38]],
|
|
puff: [[4, 39], [5, 39], [6, 39], [7, 39]],
|
|
transmogrify_flash: [[8, 39], [9, 39], [10, 39], [11, 39], [12, 39], [13, 39], [14, 39], [15, 39]],
|
|
|
|
// More custom tiles
|
|
gate_red: [0, 40],
|
|
gate_blue: [1, 40],
|
|
gate_yellow: [2, 40],
|
|
gate_green: [3, 40],
|
|
|
|
skeleton_key: [4, 40],
|
|
|
|
sand: [10, 41],
|
|
terraformer_n: [0, 43],
|
|
terraformer_e: [1, 43],
|
|
terraformer_s: [2, 43],
|
|
terraformer_w: [3, 43],
|
|
global_cycler: [4, 43],
|
|
halo: [5, 43],
|
|
fire_sticks: [6, 43],
|
|
turntable_cw: {
|
|
__special__: 'visual-state',
|
|
active: [7, 43],
|
|
inactive: [9, 43],
|
|
},
|
|
turntable_ccw: {
|
|
__special__: 'visual-state',
|
|
active: [8, 43],
|
|
inactive: [10, 43],
|
|
},
|
|
electrified_floor: {
|
|
__special__: 'visual-state',
|
|
active: [[5, 41], [6, 41], [7, 41]],
|
|
inactive: [4, 41],
|
|
},
|
|
hole: {
|
|
__special__: 'visual-state',
|
|
north: [8, 41],
|
|
open: [9, 41],
|
|
},
|
|
cracked_floor: [11, 43],
|
|
cracked_ice: [7, 40],
|
|
score_5x: [10, 40],
|
|
spikes: [5, 40],
|
|
boulder: [8, 40],
|
|
item_lock: [12, 43],
|
|
});
|
|
|
|
export const TILESET_LAYOUTS = {
|
|
'tw-static': TILE_WORLD_TILESET_LAYOUT,
|
|
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(tic = 0, perception = 'normal') {
|
|
this.tic = tic;
|
|
this.perception = perception;
|
|
|
|
// Distinguishes between interpolation of 20tps and 60fps; 3 means 20tps, 1 means 60fps
|
|
// XXX this isn't actually about update /rate/; it's about how many "frames" of cooldown
|
|
// pass between a decision and the end of a tic
|
|
this.update_rate = 3;
|
|
}
|
|
|
|
// 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;
|
|
this.animation_slowdown = 2;
|
|
}
|
|
|
|
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 === null) {
|
|
// This is explicitly never drawn (used for extra visual-only frills that don't exist in
|
|
// some tilesets)
|
|
return;
|
|
}
|
|
if (! drawspec) {
|
|
// This is just missing
|
|
console.error(`Don't know how to draw tile type ${name}!`);
|
|
return;
|
|
}
|
|
|
|
this.draw_drawspec(drawspec, name, tile, packet);
|
|
}
|
|
|
|
// 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'];
|
|
}
|
|
|
|
// Deal with animation
|
|
if (coords[0] instanceof Array) {
|
|
if (tile && tile.movement_speed) {
|
|
// This tile reports its own animation timing (in frames), so trust that, and use
|
|
// the current tic's fraction. If we're between tics, interpolate.
|
|
// FIXME if the game ever runs every frame we will have to adjust the interpolation
|
|
let p = tile.movement_progress(packet.tic % 1, packet.update_rate);
|
|
if (this.animation_slowdown > 1 && ! tile.type.ttl) {
|
|
// The players have full walk animations, but they look very silly when squeezed
|
|
// into the span of a single step, so instead we only play half at a time. The
|
|
// halves alternate, so the player still sees the whole animation when walking
|
|
// continuously. To make this work, 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.tic * 3 / tile.movement_speed) - p;
|
|
// Rounding smooths out float error (assuming the framerate never exceeds 1000)
|
|
let segment = Math.floor(Math.round(start_time * 1000) / 1000 % this.animation_slowdown);
|
|
p = (p + segment) / this.animation_slowdown;
|
|
}
|
|
// Lexy runs cooldown from S to 1; CC2 from S-1 to 0. 0 is bad, because p becomes 1
|
|
// and will overflow the cel lookup
|
|
// FIXME handle this better! it happens even to lexy
|
|
if (p >= 1) {
|
|
p = 0.999;
|
|
}
|
|
coords = coords[Math.floor(p * coords.length)];
|
|
}
|
|
else if (tile && tile.type.movement_speed) {
|
|
// This is an actor that's not moving, so use the first frame
|
|
coords = coords[0];
|
|
}
|
|
else {
|
|
// This tile animates on a global timer, one cycle every quarter of a second
|
|
coords = coords[Math.floor(packet.tic / this.animation_slowdown % 5 / 5 * coords.length)];
|
|
}
|
|
}
|
|
|
|
packet.blit(coords[0], coords[1]);
|
|
}
|
|
|
|
// 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 = 3 * this.animation_slowdown;
|
|
x += drawspec.scroll_region[0] * (packet.tic % duration / duration);
|
|
y += drawspec.scroll_region[1] * (packet.tic % duration / duration);
|
|
// 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) {
|
|
// 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) {
|
|
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 opaque and not designed to combine
|
|
if (edges === (DIRECTIONS['south'].bit | DIRECTIONS['east'].bit)) {
|
|
packet.blit(...drawspec.southeast);
|
|
}
|
|
else if (edges & DIRECTIONS['north'].bit) {
|
|
packet.blit(...drawspec.north);
|
|
}
|
|
else if (edges & DIRECTIONS['east'].bit) {
|
|
packet.blit(...drawspec.east);
|
|
}
|
|
else if (edges & DIRECTIONS['south'].bit) {
|
|
packet.blit(...drawspec.south);
|
|
}
|
|
else {
|
|
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.tic / 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's tileset has double-size art for blobs and walkers that spans the tile they're
|
|
// moving from AND the tile they're moving into.
|
|
// First, of course, this only happens if they're moving at all.
|
|
if (! tile || ! tile.movement_speed) {
|
|
this.draw_drawspec(drawspec.base, name, tile, packet);
|
|
return;
|
|
}
|
|
|
|
// They only support horizontal and vertical moves, not all four directions. The other two
|
|
// directions are simply the animations played in reverse.
|
|
let axis_cels;
|
|
let w = 1, h = 1, x = 0, y = 0, reverse = false;
|
|
if (tile.direction === 'north') {
|
|
axis_cels = drawspec.vertical;
|
|
reverse = true;
|
|
h = 2;
|
|
}
|
|
else if (tile.direction === 'south') {
|
|
axis_cels = drawspec.vertical;
|
|
h = 2;
|
|
y = -1;
|
|
}
|
|
else if (tile.direction === 'west') {
|
|
axis_cels = drawspec.horizontal;
|
|
reverse = true;
|
|
w = 2;
|
|
}
|
|
else if (tile.direction === 'east') {
|
|
axis_cels = drawspec.horizontal;
|
|
w = 2;
|
|
x = -1;
|
|
}
|
|
|
|
let p = tile.movement_progress(packet.tic % 1, packet.update_rate);
|
|
p = Math.min(p, 0.999); // FIXME hack for differing movement counters
|
|
let index = Math.floor(p * (axis_cels.length + 1));
|
|
if (index === 0 || index > axis_cels.length) {
|
|
this.draw_drawspec(drawspec.base, name, tile, packet);
|
|
}
|
|
else {
|
|
let cel = reverse ? axis_cels[axis_cels.length - index] : axis_cels[index - 1];
|
|
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') {
|
|
packet.blit(0, 3, tile.memory * 0.75, 0, 0.75, 1, 0.125, 0);
|
|
}
|
|
}
|
|
|
|
_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_drawspec(drawspec, name, tile, packet) {
|
|
if (drawspec.__special__) {
|
|
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') {
|
|
this._draw_logic_gate(drawspec, name, tile, packet);
|
|
}
|
|
else if (drawspec.__special__ === 'railroad') {
|
|
this._draw_railroad(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];
|
|
}
|
|
}
|
|
}
|