I don't know why I ever thought this was a separate pass; I think it was just the easiest way to make smooth scrolling work when I first implemented it on like day 2. Turns out it wasn't ever correct and has all manner of subtle implications I'll be sorting out for ages. This does make the turn-based stuff //way// simpler, though.
1321 lines
43 KiB
JavaScript
1321 lines
43 KiB
JavaScript
import { DIRECTIONS } from './defs.js';
|
|
import TILE_TYPES from './tiletypes.js';
|
|
|
|
// TODO really need to specify this format more concretely, whoof
|
|
// XXX special kinds of drawing i know this has for a fact:
|
|
// - letter tiles draw from a block (one of two blocks!) of half-tiles onto the center of the base
|
|
// - force floors are cropped from a double-size tile
|
|
// - wired tiles are a whole thing (floor)
|
|
// - thin walls are packed into just two tiles
|
|
// - directional blocks have arrows in an awkward layout, not 4x4 grid but actually positioned on the edges
|
|
// - green and purple toggle walls use an overlay
|
|
// - turtles use an overlay, seem to pick a tile at random every so often
|
|
// - animations are common, should maybe have configurable timing??
|
|
// - custom floors and walls /should/ be consolidated into a single tile probably
|
|
// - thin walls should probably be consolidated?
|
|
// - traps have a state
|
|
|
|
// special features i currently have
|
|
// - directions for actors, can be used anywhere
|
|
// - arrows: for directional blocks
|
|
// - mask: for thin walls (though the idea is useful in many more places)
|
|
// - wired: for wired tiles
|
|
// - overlay: for green/purple walls mostly, also some bogus cc1 tiles
|
|
|
|
// things that are currently NOT handled
|
|
// - bomb is supposed to have a fuse
|
|
// - critters should only animate when moving
|
|
// - rover animation depends on behavior, also has a quarter-tile overlay for its direction
|
|
// - slime and walkers have double-size tiles when moving
|
|
// - logic gates draw over the stuff underneath them
|
|
// - railroad tracks overlay a Lot
|
|
// - canopy, at all
|
|
// - swivel's floor (eugh)
|
|
// - xray vision
|
|
// - editor vision
|
|
export const CC2_TILESET_LAYOUT = {
|
|
'#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],
|
|
// FIXME these shouldn't be drawn with a hole when drawn in isolation, gruh
|
|
dirt_block: {
|
|
special: 'perception',
|
|
threshold: 2,
|
|
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!
|
|
base: [0, 2],
|
|
wired: [8, 26],
|
|
wired_cross: [10, 26],
|
|
is_wired_optional: true,
|
|
},
|
|
// FIXME i think these are visible with the secret eye, but my tileset puts text on them, whoops
|
|
wall_invisible: {
|
|
special: 'perception',
|
|
threshold: 2,
|
|
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',
|
|
threshold: 2,
|
|
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],
|
|
],
|
|
// FIXME these shouldn't be drawn with a hole when drawn in isolation, gruh
|
|
ice_block: {
|
|
special: 'perception',
|
|
threshold: 2,
|
|
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: [10, 3],
|
|
chip: [11, 3],
|
|
bribe: [12, 3],
|
|
speed_boots: [13, 3],
|
|
canopy: [14, 3],
|
|
// canopy xray
|
|
|
|
// TODO lit
|
|
dynamite: [0, 4],
|
|
bomb: [5, 4],
|
|
green_bomb: [6, 4],
|
|
// TODO bomb fuse tile, ugh
|
|
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',
|
|
threshold: 2,
|
|
hidden: [12, 5],
|
|
revealed: [13, 5],
|
|
},
|
|
popdown_floor_visible: [13, 5],
|
|
no_sign: [14, 5],
|
|
frame_block: {
|
|
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],
|
|
// weird translucent spiral
|
|
// TODO dopps can push but i don't think they have any other visuals
|
|
doppelganger1: {
|
|
base: [7, 6],
|
|
overlay: 'player',
|
|
},
|
|
doppelganger2: {
|
|
base: [7, 6],
|
|
overlay: 'player2',
|
|
},
|
|
button_blue: [8, 6],
|
|
button_green: [9, 6],
|
|
button_red: [10, 6],
|
|
button_brown: [11, 6],
|
|
button_pink: {
|
|
base: [0, 2],
|
|
wired: [12, 6],
|
|
},
|
|
button_black: {
|
|
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: {
|
|
base: 'green_floor',
|
|
overlay: [8, 9],
|
|
},
|
|
purple_wall: {
|
|
base: 'purple_floor',
|
|
overlay: [8, 9],
|
|
},
|
|
trap: {
|
|
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],
|
|
// FIXME i think these are visible with the secret eye, but my tileset puts text on them, whoops
|
|
fake_floor: {
|
|
special: 'perception',
|
|
threshold: 2,
|
|
hidden: [0, 10],
|
|
revealed: [10, 31],
|
|
},
|
|
// Thin walls are built piecemeal from these two tiles; the first is N/S,
|
|
// the second is E/W
|
|
thinwall_n: {
|
|
tile: [1, 10],
|
|
mask: [0, 0, 1, 0.5],
|
|
},
|
|
thinwall_s: {
|
|
tile: [1, 10],
|
|
mask: [0, 0.5, 1, 0.5],
|
|
},
|
|
thinwall_w: {
|
|
tile: [2, 10],
|
|
mask: [0, 0, 0.5, 1],
|
|
},
|
|
thinwall_e: {
|
|
tile: [2, 10],
|
|
mask: [0.5, 0, 0.5, 1],
|
|
},
|
|
thinwall_se: {
|
|
base: 'thinwall_s',
|
|
overlay: 'thinwall_e',
|
|
},
|
|
// TODO directional block arrows
|
|
teleport_blue: {
|
|
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!
|
|
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
|
|
overlay: [13, 12], // TODO also 14 + 15 for sinking
|
|
base: 'water',
|
|
},
|
|
|
|
walker: [0, 13],
|
|
// FIXME walker animations span multiple tiles
|
|
helmet: [0, 14],
|
|
stopwatch_toggle: [14, 14],
|
|
stopwatch_bonus: [15, 14],
|
|
|
|
blob: [0, 15],
|
|
// FIXME blob animations span multiple tiles
|
|
// TODO [0, 16] some kinda red/blue outline
|
|
floor_mimic: {
|
|
special: 'perception',
|
|
threshold: 1,
|
|
hidden: [0, 2],
|
|
revealed: [14, 16],
|
|
},
|
|
// TODO [15, 16] some kinda yellow/black 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], // TODO also +18 when rolling
|
|
tank_yellow: {
|
|
north: [[8, 17], [9, 17]],
|
|
east: [[10, 17], [11, 17]],
|
|
south: [[12, 17], [13, 17]],
|
|
west: [[14, 17], [15, 17]],
|
|
},
|
|
|
|
// TODO rover has an overlay showing its direction
|
|
rover: {
|
|
inert: [0, 18],
|
|
teeth: [[0, 18], [8, 18]],
|
|
glider: [
|
|
// quite fast
|
|
[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],
|
|
],
|
|
bug: [[0, 18], [1, 18], [2, 18], [3, 18], [4, 18], [5, 18], [6, 18], [7, 18]],
|
|
ball: [[0, 18], [4, 18]],
|
|
teeth_timid: [[0, 18], [9, 18]],
|
|
fireball: [
|
|
// quite fast
|
|
[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],
|
|
],
|
|
paramecium: [[7, 18], [6, 18], [5, 18], [4, 18], [3, 18], [2, 18], [1, 18], [0, 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: {
|
|
base: [0, 19],
|
|
animate_height: 1,
|
|
},
|
|
force_floor_e: {
|
|
base: [3, 19],
|
|
animate_width: -1,
|
|
},
|
|
force_floor_s: {
|
|
base: [1, 20],
|
|
animate_height: -1,
|
|
},
|
|
force_floor_w: {
|
|
base: [2, 20],
|
|
animate_width: 1,
|
|
},
|
|
teleport_green: [[4, 19], [5, 19], [6, 19], [7, 19]],
|
|
teleport_yellow: [[8, 19], [9, 19], [10, 19], [11, 19]],
|
|
transmogrifier: [[12, 19], [13, 19], [14, 19], [15, 19]],
|
|
teleport_red: {
|
|
base: [0, 2],
|
|
wired: [[4, 20], [5, 20], [6, 20], [7, 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: {
|
|
base: [14, 21],
|
|
wired: [12, 21],
|
|
},
|
|
light_switch_on: {
|
|
base: [14, 21],
|
|
wired: [13, 21],
|
|
},
|
|
thief_keys: [15, 21],
|
|
|
|
player: {
|
|
normal: {
|
|
north: [0, 22],
|
|
south: [0, 23],
|
|
west: [8, 23],
|
|
east: [8, 22],
|
|
},
|
|
blocked: 'pushing',
|
|
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: {
|
|
north: [8, 24],
|
|
east: [9, 24],
|
|
south: [10, 24],
|
|
west: [11, 24],
|
|
},
|
|
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],
|
|
},
|
|
bogus_player_win: {
|
|
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: {
|
|
overlay: [5, 5], // splash
|
|
base: 'water',
|
|
},
|
|
bogus_player_burned_fire: {
|
|
overlay: [2, 5], // explosion frame 3
|
|
base: 'fire',
|
|
},
|
|
bogus_player_burned: {
|
|
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],
|
|
},
|
|
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: {
|
|
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],
|
|
},
|
|
fire: [
|
|
[12, 29],
|
|
[13, 29],
|
|
[14, 29],
|
|
[15, 29],
|
|
],
|
|
|
|
// TODO handle train tracks! this is gonna be complicated.
|
|
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
|
|
};
|
|
|
|
// XXX need to specify that you can't use this for cc2 levels, somehow
|
|
export const TILE_WORLD_TILESET_LAYOUT = {
|
|
floor: [0, 0],
|
|
wall: [0, 1],
|
|
chip: [0, 2],
|
|
water: [0, 3],
|
|
fire: [0, 4],
|
|
wall_invisible: [0, 5],
|
|
wall_invisible_revealed: [0, 1],
|
|
thinwall_n: [0, 6],
|
|
thinwall_w: [0, 7],
|
|
thinwall_s: [0, 8],
|
|
thinwall_e: [0, 9],
|
|
// 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: {
|
|
closed: [2, 11],
|
|
open: [2, 11],
|
|
},
|
|
wall_appearing: [2, 12],
|
|
gravel: [2, 13],
|
|
popwall: [2, 14],
|
|
popwall2: [2, 14],
|
|
hint: [2, 15],
|
|
|
|
thinwall_se: [3, 0],
|
|
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: {
|
|
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],
|
|
},
|
|
};
|
|
|
|
export const LL_TILESET_LAYOUT = Object.assign({}, CC2_TILESET_LAYOUT, {
|
|
// 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, {
|
|
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, {
|
|
// TODO skating
|
|
exited: [15, 32],
|
|
burned: {
|
|
north: [12, 33],
|
|
east: [13, 33],
|
|
south: [14, 33],
|
|
west: [15, 33],
|
|
},
|
|
slimed: [1, 38],
|
|
}),
|
|
bogus_player_burned_fire: {
|
|
overlay: [6, 33],
|
|
base: 'fire',
|
|
},
|
|
bogus_player_burned: {
|
|
overlay: [6, 33],
|
|
base: 'floor',
|
|
},
|
|
|
|
// Custom tiles
|
|
popwall2: [9, 32],
|
|
gift_bow: [10, 32],
|
|
circuit_block: {
|
|
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]],
|
|
},
|
|
|
|
// Custom VFX
|
|
splash_slime: [[0, 38], [1, 38], [2, 38], [3, 38]],
|
|
});
|
|
|
|
export class Tileset {
|
|
constructor(image, layout, size_x, size_y) {
|
|
this.image = image;
|
|
this.layout = layout;
|
|
this.size_x = size_x;
|
|
this.size_y = size_y;
|
|
this.animation_slowdown = 2;
|
|
}
|
|
|
|
draw(tile, tic, blit) {
|
|
// FIXME perception
|
|
this.draw_type(tile.type.name, tile, tic, 0, blit);
|
|
}
|
|
|
|
// 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, tile, tic, blit, mask = []) {
|
|
// 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 (tic !== null) {
|
|
if (tile && tile.animation_speed) {
|
|
// This tile reports its own animation timing (in tics), so trust that, and just
|
|
// use the current tic's fraction.
|
|
// That said: adjusting animation speed complicates this slightly. Consider the
|
|
// player's walk animation, which takes 4 tics to complete, during which time we
|
|
// cycle through 8 frames. Playing that at half speed means only half the
|
|
// animation actually plays, but if the player continues walking, then on the
|
|
// NEXT four tics, we should play the other half. To make this work, use the
|
|
// tic as a global timer as well: if the animation started on tics 0-4, play the
|
|
// first half; if it started on tics 5-8, play the second half. They could get
|
|
// out of sync if the player hesitates, but no one will notice that, and this
|
|
// approach minimizes storing extra state.
|
|
let i = (tile.animation_progress + tic % 1) / tile.animation_speed;
|
|
// But do NOT do this for explosions or splashes, which have a fixed duration
|
|
// and only play once
|
|
if (this.animation_slowdown > 1 && ! tile.type.ttl) {
|
|
// i ranges from [0, 1), but a slowdown of N means we'll only play the first
|
|
// 1/N of it before the game ends (or loops) the animation.
|
|
// So increase by [0..N-1] to get it in some other range, then divide by N
|
|
// to scale back down to [0, 1)
|
|
i += Math.floor(tic / tile.animation_speed % this.animation_slowdown);
|
|
i /= this.animation_slowdown;
|
|
}
|
|
coords = coords[Math.floor(i * coords.length)];
|
|
}
|
|
else {
|
|
// This tile animates on a global timer, one cycle every quarter of a second
|
|
coords = coords[Math.floor(tic / this.animation_slowdown % 5 / 5 * coords.length)];
|
|
}
|
|
}
|
|
else {
|
|
coords = coords[0];
|
|
}
|
|
}
|
|
|
|
blit(coords[0], coords[1], ...mask);
|
|
}
|
|
|
|
_draw_letter(drawspec, tile, tic, blit) {
|
|
this._draw_standard(drawspec.base, tile, tic, blit);
|
|
|
|
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
|
|
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);
|
|
blit(x, y, 0, 0, rangedef.w, rangedef.h,
|
|
(1 - rangedef.w) / 2, (1 - rangedef.h) / 2);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_draw_logic_gate(drawspec, tile, tic, blit) {
|
|
// Layer 1: wiring state
|
|
// Always draw the unpowered wire base
|
|
let unpowered_coords = this.layout['#unpowered'];
|
|
let powered_coords = this.layout['#powered'];
|
|
blit(...unpowered_coords);
|
|
if (tile && tile.cell) {
|
|
// What goes on top varies a bit...
|
|
// FIXME implement for NOT and counter!
|
|
let r = this.layout['#wire-width'] / 2;
|
|
if (tile.cell.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);
|
|
blit(powered_coords[0], powered_coords[1], x0, y0, x1 - x0, y1 - y0);
|
|
}
|
|
if (tile.cell.powered_edges & DIRECTIONS[DIRECTIONS[tile.direction].right].bit) {
|
|
// Right input, which includes the middle
|
|
let [x0, y0, x1, y1] = this._rotate(tile.direction, 0.5 - r, 0.5 - r, 1, 1);
|
|
blit(powered_coords[0], powered_coords[1], x0, y0, x1 - x0, y1 - y0);
|
|
}
|
|
if (tile.cell.powered_edges & DIRECTIONS[DIRECTIONS[tile.direction].left].bit) {
|
|
// Left input, which does not include the middle
|
|
let [x0, y0, x1, y1] = this._rotate(tile.direction, 0, 0.5 - r, 0.5 - r, 1);
|
|
blit(powered_coords[0], powered_coords[1], x0, y0, x1 - x0, y1 - y0);
|
|
}
|
|
}
|
|
|
|
// Layer 2: the tile itself
|
|
this._draw_standard(drawspec.logic_gate_tiles[tile.gate_type], tile, tic, blit);
|
|
|
|
// Layer 3: counter number
|
|
if (tile.gate_type === 'counter') {
|
|
blit(0, 3, tile.memory * 0.75, 0, 0.75, 1, 0.125, 0);
|
|
}
|
|
}
|
|
|
|
_draw_railroad(drawspec, tile, tic, blit) {
|
|
// All railroads have regular gravel underneath
|
|
// TODO would be nice to disambiguate since it's possible to have nothing visible
|
|
this._draw_standard(this.layout['gravel'], tile, tic, blit);
|
|
|
|
// 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_standard(drawspec.railroad_ties[part], tile, tic, blit);
|
|
}
|
|
let tracks = has_switch ? drawspec.railroad_inactive : drawspec.railroad_active;
|
|
for (let part of visible_parts) {
|
|
if (part !== topmost_part) {
|
|
this._draw_standard(tracks[part], tile, tic, blit);
|
|
}
|
|
}
|
|
|
|
if (topmost_part) {
|
|
this._draw_standard(drawspec.railroad_active[topmost_part], tile, tic, blit);
|
|
}
|
|
if (has_switch) {
|
|
this._draw_standard(drawspec.railroad_switch, tile, tic, blit);
|
|
}
|
|
}
|
|
|
|
// Draws a tile type, given by name. Passing in a tile is optional, but
|
|
// without it you'll get defaults.
|
|
draw_type(name, tile, tic, perception, blit) {
|
|
let drawspec = this.layout[name];
|
|
if (! drawspec) {
|
|
console.error(`Don't know how to draw tile type ${name}!`);
|
|
return;
|
|
}
|
|
|
|
if (drawspec.overlay) {
|
|
// Goofy overlay thing used for green/purple toggle tiles and
|
|
// southeast thin walls. Draw the base (a type name or drawspec), then draw
|
|
// the overlay (either a type name or a regular draw spec).
|
|
// TODO chance of infinite recursion here
|
|
if (typeof drawspec.base === 'string') {
|
|
this.draw_type(drawspec.base, tile, tic, perception, blit);
|
|
}
|
|
else {
|
|
this._draw_standard(drawspec.base, tile, tic, blit);
|
|
}
|
|
if (typeof drawspec.overlay === 'string') {
|
|
this.draw_type(drawspec.overlay, tile, tic, perception, blit);
|
|
return;
|
|
}
|
|
else {
|
|
drawspec = drawspec.overlay;
|
|
}
|
|
}
|
|
|
|
// TODO shift everything to use this style, this is ridiculous
|
|
if (drawspec.special) {
|
|
if (drawspec.special === 'letter') {
|
|
this._draw_letter(drawspec, tile, tic, blit);
|
|
return;
|
|
}
|
|
else if (drawspec.special === 'perception') {
|
|
if (perception >= drawspec.threshold) {
|
|
drawspec = drawspec.revealed;
|
|
}
|
|
else {
|
|
drawspec = drawspec.hidden;
|
|
}
|
|
}
|
|
else if (drawspec.special === 'logic-gate') {
|
|
this._draw_logic_gate(drawspec, tile, tic, blit);
|
|
return;
|
|
}
|
|
else if (drawspec.special === 'railroad') {
|
|
this._draw_railroad(drawspec, tile, tic, blit);
|
|
return;
|
|
}
|
|
}
|
|
|
|
let coords = drawspec;
|
|
if (drawspec.mask) {
|
|
// Some tiles (OK, just the thin walls) don't actually draw a full
|
|
// tile, so some adjustments are needed; see below
|
|
coords = drawspec.tile;
|
|
}
|
|
else if (drawspec.wired) {
|
|
// This /should/ match CC2's draw order exactly, based on experimentation
|
|
let wire_radius = this.layout['#wire-width'] / 2;
|
|
if (tile && tile.wire_directions === 0x0f) {
|
|
// This is a wired tile with crossing wires, which acts a little differently
|
|
// Draw the base tile
|
|
blit(drawspec.base[0], drawspec.base[1]);
|
|
|
|
// Draw the two wires as separate rectangles, NS then EW
|
|
let wire_inset = 0.5 - wire_radius;
|
|
let wire_coords_ns = this.layout[
|
|
tile.cell && tile.cell.powered_edges & DIRECTIONS['north'].bit ? '#powered' : '#unpowered'];
|
|
let wire_coords_ew = this.layout[
|
|
tile.cell && tile.cell.powered_edges & DIRECTIONS['east'].bit ? '#powered' : '#unpowered'];
|
|
blit(wire_coords_ns[0], wire_coords_ns[1], wire_inset, 0, wire_radius * 2, 1);
|
|
blit(wire_coords_ew[0], wire_coords_ew[1], 0, wire_inset, 1, wire_radius * 2);
|
|
|
|
// Draw the cross tile on top
|
|
coords = drawspec.wired_cross ?? drawspec.wired;
|
|
}
|
|
else if (tile && tile.wire_directions) {
|
|
// Draw the base tile
|
|
blit(drawspec.base[0], drawspec.base[1]);
|
|
|
|
// Draw the wire part as a single rectangle, initially just a small dot in the
|
|
// center, but extending out to any edge that has a wire present
|
|
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 (tile.wire_directions & DIRECTIONS['north'].bit) {
|
|
y0 = 0;
|
|
}
|
|
if (tile.wire_directions & DIRECTIONS['east'].bit) {
|
|
x1 = 1;
|
|
}
|
|
if (tile.wire_directions & DIRECTIONS['south'].bit) {
|
|
y1 = 1;
|
|
}
|
|
if (tile.wire_directions & DIRECTIONS['west'].bit) {
|
|
x0 = 0;
|
|
}
|
|
let wire_coords = this.layout[tile.cell && tile.cell.powered_edges ? '#powered' : '#unpowered'];
|
|
blit(wire_coords[0], wire_coords[1], x0, y0, x1 - x0, y1 - y0);
|
|
|
|
// Then draw the wired tile on top of it all
|
|
coords = drawspec.wired;
|
|
}
|
|
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) {
|
|
coords = drawspec.base;
|
|
}
|
|
else {
|
|
blit(drawspec.base[0], drawspec.base[1]);
|
|
coords = drawspec.wired;
|
|
}
|
|
}
|
|
}
|
|
else if (drawspec.arrows) {
|
|
// Directional blocks have a specific overlay, but draw the base first
|
|
coords = drawspec.base;
|
|
}
|
|
else if (drawspec.animate_width) {
|
|
// Force floors animate their... cutout, I guess?
|
|
let [x, y] = drawspec.base;
|
|
let duration = 3 * this.animation_slowdown;
|
|
x += drawspec.animate_width * (tic % duration / duration);
|
|
// Round to tile width
|
|
x = Math.floor(x * this.size_x + 0.5) / this.size_x;
|
|
coords = [x, y];
|
|
}
|
|
else if (drawspec.animate_height) {
|
|
// Same, but along the other axis
|
|
let [x, y] = drawspec.base;
|
|
let duration = 3 * this.animation_slowdown;
|
|
y += drawspec.animate_height * (tic % duration / duration);
|
|
// Round to tile height
|
|
y = Math.floor(y * this.size_y + 0.5) / this.size_y;
|
|
coords = [x, y];
|
|
}
|
|
|
|
// Apply custom per-type visual states
|
|
if (TILE_TYPES[name] && TILE_TYPES[name].visual_state) {
|
|
// 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 coords[state] === 'string') {
|
|
coords = coords[coords[state]];
|
|
}
|
|
else {
|
|
coords = coords[state];
|
|
}
|
|
|
|
if (! coords) {
|
|
console.warn("No such state", state, "for tile", name, tile);
|
|
}
|
|
}
|
|
|
|
// Generic sprite definitions from here on!
|
|
// If we still have an object, it must be a table of directions
|
|
if (!(coords instanceof Array)) {
|
|
coords = coords[(tile && tile.direction) ?? 'south'];
|
|
}
|
|
|
|
// Deal with animation
|
|
if (coords[0] instanceof Array) {
|
|
if (tic !== null) {
|
|
if (tile && tile.animation_speed) {
|
|
// This tile reports its own animation timing (in tics), so trust that, and just
|
|
// use the current tic's fraction.
|
|
// That said: adjusting animation speed complicates this slightly. Consider the
|
|
// player's walk animation, which takes 4 tics to complete, during which time we
|
|
// cycle through 8 frames. Playing that at half speed means only half the
|
|
// animation actually plays, but if the player continues walking, then on the
|
|
// NEXT four tics, we should play the other half. To make this work, use the
|
|
// tic as a global timer as well: if the animation started on tics 0-4, play the
|
|
// first half; if it started on tics 5-8, play the second half. They could get
|
|
// out of sync if the player hesitates, but no one will notice that, and this
|
|
// approach minimizes storing extra state.
|
|
let i = (tile.animation_progress + tic % 1) / tile.animation_speed;
|
|
// But do NOT do this for explosions or splashes, which have a fixed duration
|
|
// and only play once
|
|
if (this.animation_slowdown > 1 && ! tile.type.ttl) {
|
|
// i ranges from [0, 1), but a slowdown of N means we'll only play the first
|
|
// 1/N of it before the game ends (or loops) the animation.
|
|
// So increase by [0..N-1] to get it in some other range, then divide by N
|
|
// to scale back down to [0, 1)
|
|
i += Math.floor(tic / tile.animation_speed % this.animation_slowdown);
|
|
i /= this.animation_slowdown;
|
|
}
|
|
coords = coords[Math.floor(i * coords.length)];
|
|
}
|
|
else {
|
|
// This tile animates on a global timer, one cycle every quarter of a second
|
|
coords = coords[Math.floor(tic / this.animation_slowdown % 5 / 5 * coords.length)];
|
|
}
|
|
}
|
|
else {
|
|
coords = coords[0];
|
|
}
|
|
}
|
|
|
|
if (drawspec.mask) {
|
|
// Continue on with masking
|
|
coords = drawspec.tile;
|
|
let [x0, y0, w, h] = drawspec.mask;
|
|
blit(coords[0], coords[1], x0, y0, w, h);
|
|
}
|
|
else {
|
|
if (!coords) console.error(name, tile);
|
|
blit(coords[0], coords[1]);
|
|
}
|
|
|
|
// Wired tiles may also have tunnels, drawn on top of everything else
|
|
if (drawspec.wired && 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) {
|
|
blit(tunnel_coords[0], tunnel_coords[1],
|
|
tunnel_offset, 0, tunnel_width, tunnel_length);
|
|
}
|
|
if (tile.wire_tunnel_directions & DIRECTIONS['south'].bit) {
|
|
blit(tunnel_coords[0], tunnel_coords[1],
|
|
tunnel_offset, 1 - tunnel_length, tunnel_width, tunnel_length);
|
|
}
|
|
if (tile.wire_tunnel_directions & DIRECTIONS['west'].bit) {
|
|
blit(tunnel_coords[0], tunnel_coords[1],
|
|
0, tunnel_offset, tunnel_length, tunnel_width);
|
|
}
|
|
if (tile.wire_tunnel_directions & DIRECTIONS['east'].bit) {
|
|
blit(tunnel_coords[0], tunnel_coords[1],
|
|
1 - tunnel_length, tunnel_offset, tunnel_length, tunnel_width);
|
|
}
|
|
}
|
|
|
|
// Directional blocks have arrows drawn on top
|
|
// TODO does cc2 draw even if there are no arrows?
|
|
if (drawspec.arrows && 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;
|
|
}
|
|
blit(x, y, x0, y0, x1 - x0, y1 - y0);
|
|
}
|
|
}
|
|
|
|
_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];
|
|
}
|
|
}
|
|
}
|