import { DIRECTIONS } from './defs.js'; import { random_choice } from './util.js'; // Draw layers const LAYER_TERRAIN = 0; const LAYER_ITEM = 1; const LAYER_ITEM_MOD = 2; const LAYER_ACTOR = 3; const LAYER_OVERLAY = 4; // TODO cc2 order is: swivel, thinwalls, canopy (and yes you can have them all in the same tile) function on_ready_force_floor(me, level) { // At the start of the level, if there's an actor on a force floor: // - use on_arrive to set the actor's direction // - set the slide_mode (normally done by the main game loop) // - item bestowal: if they're being pushed into a wall and standing on an item, pick up the // item, even if they couldn't normally pick items up let actor = me.cell.get_actor(); if (! actor) return; me.type.on_arrive(me, level, actor); if (me.type.slide_mode) { actor.slide_mode = me.type.slide_mode; } // Item bestowal // TODO seemingly lynx/cc2 only pick RFF direction at decision time, but that's in conflict with // doing this here; decision time hasn't happened yet, but we need to know what direction we're // moving to know whether bestowal happens? so what IS the cause of item bestowal? let neighbor = level.get_neighboring_cell(me.cell, actor.direction); if (! neighbor) return; if (! neighbor.blocks_entering(actor, actor.direction, level, true)) return; let item = me.cell.get_item(); if (! item) return; if (level.attempt_take(actor, item) && actor.ignores(me.type.name)) { // If they just picked up suction boots, they're no longer sliding // TODO this feels hacky, shouldn't the slide mode be erased some other way? actor.slide_mode = null; } } function blocks_leaving_thin_walls(me, actor, direction) { return me.type.thin_walls.has(direction); } function player_visual_state(me) { if (! me) { return 'normal'; } if (me.fail_reason === 'drowned') { return 'drowned'; } else if (me.fail_reason === 'burned') { return 'burned'; } else if (me.fail_reason === 'exploded') { return 'exploded'; } else if (me.fail_reason) { return 'failed'; } else if (me.cell && (me.previous_cell || me.cell).some(t => t.type.name === 'water')) { // CC2 shows a swimming pose while still in water, or moving away from water return 'swimming'; } else if (me.slide_mode === 'ice') { return 'skating'; } else if (me.slide_mode === 'force') { return 'forced'; } else if (me.is_blocked) { return 'blocked'; } else if (me.is_pushing) { return 'pushing'; } else if (me.animation_speed) { return 'moving'; } else { return 'normal'; } } const TILE_TYPES = { // Floors and walls floor: { draw_layer: LAYER_TERRAIN, }, floor_letter: { draw_layer: LAYER_TERRAIN, }, floor_custom_green: { draw_layer: LAYER_TERRAIN, }, floor_custom_pink: { draw_layer: LAYER_TERRAIN, }, floor_custom_yellow: { draw_layer: LAYER_TERRAIN, }, floor_custom_blue: { draw_layer: LAYER_TERRAIN, }, wall: { draw_layer: LAYER_TERRAIN, blocks_all: true, }, wall_custom_green: { draw_layer: LAYER_TERRAIN, blocks_all: true, }, wall_custom_pink: { draw_layer: LAYER_TERRAIN, blocks_all: true, }, wall_custom_yellow: { draw_layer: LAYER_TERRAIN, blocks_all: true, }, wall_custom_blue: { draw_layer: LAYER_TERRAIN, blocks_all: true, }, wall_invisible: { draw_layer: LAYER_TERRAIN, // FIXME cc2 seems to make these flicker briefly blocks_all: true, }, wall_appearing: { draw_layer: LAYER_TERRAIN, blocks_all: true, on_bump(me, level, other) { if (other.type.can_reveal_walls) { level.transmute_tile(me, 'wall'); } }, }, popwall: { draw_layer: LAYER_TERRAIN, blocks_monsters: true, blocks_blocks: true, on_ready(me, level) { if (level.compat.auto_convert_ccl_popwalls && me.cell.some(tile => tile.type.is_actor)) { // Fix blocks and other actors on top of popwalls by turning them into double // popwalls, which preserves CC2 popwall behavior me.type = TILE_TYPES['popwall2']; } }, on_depart(me, level, other) { level.transmute_tile(me, 'wall'); }, }, // LL specific tile that can only be stepped on /twice/, originally used to repair differences // with popwall behavior between Lynx and Steam popwall2: { draw_layer: LAYER_TERRAIN, blocks_monsters: true, blocks_blocks: true, on_depart(me, level, other) { level.transmute_tile(me, 'popwall'); }, }, // FIXME in a cc1 tileset, these tiles are opaque >:S thinwall_n: { draw_layer: LAYER_OVERLAY, thin_walls: new Set(['north']), blocks_leaving: blocks_leaving_thin_walls, }, thinwall_s: { draw_layer: LAYER_OVERLAY, thin_walls: new Set(['south']), blocks_leaving: blocks_leaving_thin_walls, }, thinwall_e: { draw_layer: LAYER_OVERLAY, thin_walls: new Set(['east']), blocks_leaving: blocks_leaving_thin_walls, }, thinwall_w: { draw_layer: LAYER_OVERLAY, thin_walls: new Set(['west']), blocks_leaving: blocks_leaving_thin_walls, }, thinwall_se: { draw_layer: LAYER_OVERLAY, thin_walls: new Set(['south', 'east']), blocks_leaving: blocks_leaving_thin_walls, }, fake_wall: { draw_layer: LAYER_TERRAIN, blocks_all: true, on_ready(me, level) { if (level.compat.auto_convert_ccl_blue_walls && me.cell.some(tile => tile.type.is_actor)) { // Blocks can be pushed off of blue walls in TW Lynx, which only works due to a tiny // quirk of the engine that I don't want to replicate, so replace them with popwalls me.type = TILE_TYPES['popwall']; } }, on_bump(me, level, other) { if (other.type.can_reveal_walls) { level.transmute_tile(me, 'wall'); } }, }, fake_floor: { draw_layer: LAYER_TERRAIN, blocks_monsters: true, blocks_blocks: true, on_bump(me, level, other) { if (other.type.can_reveal_walls) { level.transmute_tile(me, 'floor'); } }, }, popdown_wall: { draw_layer: LAYER_TERRAIN, blocks_all: true, }, popdown_floor: { draw_layer: LAYER_TERRAIN, blocks_blocks: true, // FIXME should be on_approach on_arrive(me, level, other) { // FIXME could probably do this with state? or, eh level.transmute_tile(me, 'popdown_floor_visible'); }, }, popdown_floor_visible: { draw_layer: LAYER_TERRAIN, blocks_blocks: true, on_depart(me, level, other) { // FIXME possibly changes back too fast, not even visible for a tic for me (much like stepping on a button oops) level.transmute_tile(me, 'popdown_floor'); }, }, // TODO these also block the corresponding mirror actors no_player1_sign: { draw_layer: LAYER_TERRAIN, blocks(me, level, other) { return (other.type.name === 'player'); }, }, no_player2_sign: { draw_layer: LAYER_TERRAIN, blocks(me, level, other) { return (other.type.name === 'player2'); }, }, steel: { draw_layer: LAYER_TERRAIN, blocks_all: true, }, canopy: { draw_layer: LAYER_OVERLAY, }, // Swivel doors swivel_floor: { draw_layer: LAYER_TERRAIN, }, swivel_ne: { draw_layer: LAYER_OVERLAY, thin_walls: new Set(['north', 'east']), on_depart(me, level, other) { if (other.direction === 'north') { level.transmute_tile(me, 'swivel_se'); } else if (other.direction === 'east') { level.transmute_tile(me, 'swivel_nw'); } }, }, swivel_se: { draw_layer: LAYER_OVERLAY, thin_walls: new Set(['south', 'east']), on_depart(me, level, other) { if (other.direction === 'south') { level.transmute_tile(me, 'swivel_ne'); } else if (other.direction === 'east') { level.transmute_tile(me, 'swivel_sw'); } }, }, swivel_sw: { draw_layer: LAYER_OVERLAY, thin_walls: new Set(['south', 'west']), on_depart(me, level, other) { if (other.direction === 'south') { level.transmute_tile(me, 'swivel_nw'); } else if (other.direction === 'west') { level.transmute_tile(me, 'swivel_se'); } }, }, swivel_nw: { draw_layer: LAYER_OVERLAY, thin_walls: new Set(['north', 'west']), on_depart(me, level, other) { if (other.direction === 'north') { level.transmute_tile(me, 'swivel_sw'); } else if (other.direction === 'west') { level.transmute_tile(me, 'swivel_ne'); } }, }, // Railroad railroad: { draw_layer: LAYER_TERRAIN, // TODO a lot!! }, // Locked doors door_red: { draw_layer: LAYER_TERRAIN, blocks(me, level, other) { // TODO not quite sure if this one is right; there are complex interactions with monsters, e.g. most monsters can eat blue keys but can't actually use them return ! (other.type.has_inventory && other.has_item('key_red')); }, on_arrive(me, level, other) { if (level.take_key_from_actor(other, 'key_red')) { level.sfx.play_once('door', me.cell); level.transmute_tile(me, 'floor'); } }, }, door_blue: { draw_layer: LAYER_TERRAIN, blocks(me, level, other) { return ! (other.type.has_inventory && other.has_item('key_blue')); }, on_arrive(me, level, other) { if (level.take_key_from_actor(other, 'key_blue')) { level.sfx.play_once('door', me.cell); level.transmute_tile(me, 'floor'); } }, }, door_yellow: { draw_layer: LAYER_TERRAIN, blocks(me, level, other) { return ! (other.type.has_inventory && other.has_item('key_yellow')); }, on_arrive(me, level, other) { if (level.take_key_from_actor(other, 'key_yellow')) { level.sfx.play_once('door', me.cell); level.transmute_tile(me, 'floor'); } }, }, door_green: { draw_layer: LAYER_TERRAIN, blocks(me, level, other) { return ! (other.type.has_inventory && other.has_item('key_green')); }, on_arrive(me, level, other) { if (level.take_key_from_actor(other, 'key_green')) { level.sfx.play_once('door', me.cell); level.transmute_tile(me, 'floor'); } }, }, // Terrain dirt: { draw_layer: LAYER_TERRAIN, blocks_monsters: true, blocks_blocks: true, blocks(me, level, other) { return (other.type.name === 'player2' && ! other.has_item('hiking_boots')); }, on_arrive(me, level, other) { level.transmute_tile(me, 'floor'); }, }, gravel: { draw_layer: LAYER_TERRAIN, blocks_monsters: true, blocks(me, level, other) { return (other.type.name === 'player2' && ! other.has_item('hiking_boots')); }, }, // Hazards fire: { draw_layer: LAYER_TERRAIN, blocks(me, level, other) { return (other.type.is_monster && other.type.name !== 'fireball'); }, on_arrive(me, level, other) { if (other.type.name === 'ice_block') { level.remove_tile(other); level.transmute_tile(me, 'water'); } else if (other.type.is_player) { level.fail('burned'); } else { level.remove_tile(other); } }, }, water: { draw_layer: LAYER_TERRAIN, on_arrive(me, level, other) { // TODO cc1 allows items under water, i think; water was on the upper layer level.sfx.play_once('splash', me.cell); if (other.type.name === 'dirt_block') { level.transmute_tile(other, 'splash'); level.transmute_tile(me, 'dirt'); } else if (other.type.name === 'directional_block') { level.transmute_tile(other, 'splash'); level.transmute_tile(me, 'floor'); } else if (other.type.name === 'ice_block') { level.transmute_tile(other, 'splash'); level.transmute_tile(me, 'ice'); } else if (other.type.is_player) { level.fail('drowned'); } else { level.transmute_tile(other, 'splash'); } }, }, turtle: { draw_layer: LAYER_TERRAIN, on_depart(me, level, other) { level.transmute_tile(me, 'water'); // TODO feels like we should spawn water underneath us, then transmute ourselves into the splash? level.spawn_animation(me.cell, 'splash'); }, }, ice: { draw_layer: LAYER_TERRAIN, slide_mode: 'ice', }, ice_sw: { draw_layer: LAYER_TERRAIN, thin_walls: new Set(['south', 'west']), slide_mode: 'ice', blocks_leaving: blocks_leaving_thin_walls, on_arrive(me, level, other) { if (other.direction === 'south') { level.set_actor_direction(other, 'east'); } else { level.set_actor_direction(other, 'north'); } }, }, ice_nw: { draw_layer: LAYER_TERRAIN, thin_walls: new Set(['north', 'west']), slide_mode: 'ice', blocks_leaving: blocks_leaving_thin_walls, on_arrive(me, level, other) { if (other.direction === 'north') { level.set_actor_direction(other, 'east'); } else { level.set_actor_direction(other, 'south'); } }, }, ice_ne: { draw_layer: LAYER_TERRAIN, thin_walls: new Set(['north', 'east']), slide_mode: 'ice', blocks_leaving: blocks_leaving_thin_walls, on_arrive(me, level, other) { if (other.direction === 'north') { level.set_actor_direction(other, 'west'); } else { level.set_actor_direction(other, 'south'); } }, }, ice_se: { draw_layer: LAYER_TERRAIN, thin_walls: new Set(['south', 'east']), slide_mode: 'ice', blocks_leaving: blocks_leaving_thin_walls, on_arrive(me, level, other) { if (other.direction === 'south') { level.set_actor_direction(other, 'west'); } else { level.set_actor_direction(other, 'north'); } }, }, force_floor_n: { draw_layer: LAYER_TERRAIN, slide_mode: 'force', on_ready: on_ready_force_floor, on_arrive(me, level, other) { level.set_actor_direction(other, 'north'); }, }, force_floor_e: { draw_layer: LAYER_TERRAIN, slide_mode: 'force', on_ready: on_ready_force_floor, on_arrive(me, level, other) { level.set_actor_direction(other, 'east'); }, }, force_floor_s: { draw_layer: LAYER_TERRAIN, slide_mode: 'force', on_ready: on_ready_force_floor, on_arrive(me, level, other) { level.set_actor_direction(other, 'south'); }, }, force_floor_w: { draw_layer: LAYER_TERRAIN, slide_mode: 'force', on_ready: on_ready_force_floor, on_arrive(me, level, other) { level.set_actor_direction(other, 'west'); }, }, force_floor_all: { draw_layer: LAYER_TERRAIN, slide_mode: 'force', on_ready: on_ready_force_floor, // TODO ms: this is random, and an acting wall to monsters (!) // TODO lynx/cc2 check this at decision time, which may affect ordering on_arrive(me, level, other) { level.set_actor_direction(other, level.get_force_floor_direction()); }, }, slime: { draw_layer: LAYER_TERRAIN, // FIXME kills everything except ghosts, blobs, blocks // FIXME blobs spread slime onto floor tiles, even destroying wiring on_arrive(me, level, other) { if (other.type.name === 'dirt_block' || other.type.name === 'ice_block') { level.transmute_tile(me, 'floor'); } }, }, bomb: { draw_layer: LAYER_ITEM, on_arrive(me, level, other) { level.remove_tile(me); if (other.type.is_player) { level.fail('exploded'); } else { level.sfx.play_once('bomb', me.cell); level.transmute_tile(other, 'explosion'); } }, }, thief_tools: { draw_layer: LAYER_TERRAIN, blocks_monsters: true, blocks_blocks: true, on_arrive(me, level, other) { level.sfx.play_once('thief', me.cell); level.take_all_tools_from_actor(other); if (other.type.is_player) { level.adjust_bonus(0, 0.5); } }, }, thief_keys: { draw_layer: LAYER_TERRAIN, blocks_monsters: true, blocks_blocks: true, on_arrive(me, level, other) { level.sfx.play_once('thief', me.cell); level.take_all_keys_from_actor(other); if (other.type.is_player) { level.adjust_bonus(0, 0.5); } }, }, no_sign: { draw_layer: LAYER_ITEM_MOD, disables_pickup: true, blocks(me, level, other) { let item; for (let tile of me.cell) { if (tile.type.is_item) { item = tile.type.name; break; } } return item && other.has_item(item); }, }, bestowal_bow: { draw_layer: LAYER_ITEM_MOD, allows_all_pickup: true, }, // Mechanisms dirt_block: { draw_layer: LAYER_ACTOR, blocks_all: true, is_actor: true, is_block: true, ignores: new Set(['fire', 'flame_jet_on']), movement_speed: 4, }, ice_block: { draw_layer: LAYER_ACTOR, blocks_all: true, is_actor: true, is_block: true, can_reveal_walls: true, movement_speed: 4, pushes: { ice_block: true, directional_block: true, }, }, directional_block: { // TODO directional, obviously // TODO floor in water // TODO destroyed in slime // TODO rotate on train tracks draw_layer: LAYER_ACTOR, blocks_all: true, is_actor: true, is_block: true, can_reveal_walls: true, movement_speed: 4, allows_push(me, direction) { return me.arrows && me.arrows.has(direction); }, pushes: { dirt_block: true, ice_block: true, directional_block: true, }, }, green_floor: { draw_layer: LAYER_TERRAIN, on_gray_button(me, level) { level.transmute_tile(me, 'green_wall'); }, }, green_wall: { draw_layer: LAYER_TERRAIN, blocks_all: true, on_gray_button(me, level) { level.transmute_tile(me, 'green_floor'); }, }, green_chip: { draw_layer: LAYER_ITEM, is_chip: true, is_required_chip: true, blocks_monsters: true, blocks_blocks: true, on_arrive(me, level, other) { if (other.type.is_player) { level.collect_chip(); level.remove_tile(me); } }, // Not affected by gray buttons }, green_bomb: { draw_layer: LAYER_ITEM, is_required_chip: true, on_arrive(me, level, other) { level.remove_tile(me); if (other.type.is_player) { level.fail('exploded'); } else { level.sfx.play_once('bomb', me.cell); level.transmute_tile(other, 'explosion'); } }, // Not affected by gray buttons }, purple_floor: { draw_layer: LAYER_TERRAIN, on_gray_button(me, level) { level.transmute_tile(me, 'purple_wall'); }, on_power(me, level) { me.type.on_gray_button(me, level); }, on_depower(me, level) { me.type.on_gray_button(me, level); }, }, purple_wall: { draw_layer: LAYER_TERRAIN, blocks_all: true, on_gray_button(me, level) { level.transmute_tile(me, 'purple_floor'); }, on_power(me, level) { me.type.on_gray_button(me, level); }, on_depower(me, level) { me.type.on_gray_button(me, level); }, }, cloner: { draw_layer: LAYER_TERRAIN, // TODO not the case for an empty one in cc2, apparently blocks_all: true, traps(me, actor) { return ! actor._clone_release; }, activate(me, level) { let actor = me.cell.get_actor(); if (! actor) return; // Copy this stuff in case the movement changes it // TODO should anything else be preserved? let type = actor.type; let direction = actor.direction; // Unstick and try to move the actor; if it's blocked, abort the clone. // This temporary flag tells us to let it leave; it doesn't need to be undoable, since // it doesn't persist for more than a tic actor._clone_release = true; if (level.attempt_step(actor, direction)) { // FIXME add this underneath, just above the cloner let new_template = new actor.constructor(type, direction); level.add_tile(new_template, me.cell); level.add_actor(new_template); } delete actor._clone_release; }, // Also clones on rising pulse or gray button on_power(me, level) { me.type.activate(me, level); }, on_gray_button(me, level) { me.type.activate(me, level); }, }, trap: { draw_layer: LAYER_TERRAIN, add_press_ready(me, level, other) { // Same as below, but without ejection me.presses = (me.presses ?? 0) + 1; }, // Lynx (not cc2): open traps immediately eject their contents on arrival, if possible add_press(me, level) { level._set_tile_prop(me, 'presses', (me.presses ?? 0) + 1); if (me.presses === 1) { // Free everything on us, if we went from 0 to 1 presses (i.e. closed to open) for (let tile of Array.from(me.cell)) { if (tile.type.is_actor) { // Forcibly move anything released from a trap, to keep it in sync with // whatever pushed the button level.attempt_step(tile, tile.direction); } } } }, remove_press(me, level) { level._set_tile_prop(me, 'presses', me.presses - 1); }, traps(me, actor) { return ! me.presses; }, on_power(me, level) { // Treat being powered or not as an extra kind of brown button press me.type.add_press(me, level); }, on_depower(me, level) { me.type.remove_press(me, level); }, visual_state(me) { if (me && me.presses) { return 'open'; } else { return 'closed'; } }, }, transmogrifier: { draw_layer: LAYER_TERRAIN, _mogrifications: { player: 'player2', player2: 'player', // TODO mirror players too dirt_block: 'ice_block', ice_block: 'dirt_block', ball: 'walker', walker: 'ball', fireball: 'bug', bug: 'glider', glider: 'paramecium', paramecium: 'fireball', tank_blue: 'tank_yellow', tank_yellow: 'tank_blue', // TODO teeth, timid teeth }, _blob_mogrifications: ['ball', 'walker', 'fireball', 'glider', 'paramecium', 'bug', 'tank_blue', 'teeth', /* TODO 'timid_teeth' */ ], // TODO can be wired, in which case only works when powered; other minor concerns, see wiki on_arrive(me, level, other) { let name = other.type.name; if (me.type._mogrifications[name]) { level.transmute_tile(other, me.type._mogrifications[name]); } else if (name === 'blob') { // TODO how is this randomness determined? important for replays! let options = me.type._blob_mogrifications; level.transmute_tile(other, options[Math.floor(Math.random() * options.length)]); } }, }, teleport_blue: { draw_layer: LAYER_TERRAIN, teleport_dest_order(me, level, other) { return level.iter_tiles_in_reading_order(me.cell, 'teleport_blue', true); }, }, teleport_red: { draw_layer: LAYER_TERRAIN, teleport_try_all_directions: true, teleport_allow_override: true, teleport_dest_order(me, level, other) { return level.iter_tiles_in_reading_order(me.cell, 'teleport_red'); }, }, teleport_green: { draw_layer: LAYER_TERRAIN, teleport_try_all_directions: true, teleport_dest_order(me, level, other) { let all = Array.from(level.iter_tiles_in_reading_order(me.cell, 'teleport_green')); if (all.length <= 1) { // If this is the only teleporter, just walk out the other side — and, crucially, do // NOT advance the PRNG return [me]; } // Note the iterator starts on the /next/ teleporter, so there's an implicit +1 here. // The -1 is to avoid spitting us back out of the same teleporter, which will be last in // the list let target = all[level.prng() % (all.length - 1)]; // Also set the actor's (initial) exit direction level.set_actor_direction(other, ['north', 'east', 'south', 'west'][level.prng() % 4]); return [target, me]; }, }, teleport_yellow: { draw_layer: LAYER_TERRAIN, teleport_allow_override: true, teleport_dest_order(me, level, other) { // FIXME special pickup behavior; NOT an item though, does not combine with no sign return level.iter_tiles_in_reading_order(me.cell, 'teleport_yellow', true); }, }, // Flame jet rules: // - State toggles /while/ an orange button is held or wire current is received // - Multiple such inputs cancel each other out // - Gray button toggles it permanently flame_jet_off: { draw_layer: LAYER_TERRAIN, activate(me, level) { level.transmute_tile(me, 'flame_jet_on'); }, on_gray_button(me, level) { me.type.activate(me, level); }, on_power(me, level) { me.type.activate(me, level); }, }, flame_jet_on: { draw_layer: LAYER_TERRAIN, activate(me, level) { level.transmute_tile(me, 'flame_jet_off'); }, on_gray_button(me, level) { me.type.activate(me, level); }, on_power(me, level) { me.type.activate(me, level); }, // Kill anything that shows up // FIXME every tic, also kills every actor in the cell (mostly matters if you step on with // fire boots and then drop them) on_arrive(me, level, other) { // Note that blocks, fireballs, and anything with fire boots are immune // TODO would be neat if this understood "ignores anything with fire immunity" but that // might be a bit too high-level for this game if (other.type.is_player) { level.fail('burned'); } else { // TODO should this play a sound? level.transmute_tile(other, 'explosion'); } }, }, // Buttons button_blue: { draw_layer: LAYER_TERRAIN, on_arrive(me, level, other) { level.sfx.play_once('button-press', me.cell); // Flip direction of all blue tanks for (let actor of level.actors) { // TODO generify somehow?? if (actor.type.name === 'tank_blue') { level._set_tile_prop(actor, 'pending_reverse', ! actor.pending_reverse); } } }, on_depart(me, level, other) { level.sfx.play_once('button-release', me.cell); }, }, button_yellow: { draw_layer: LAYER_TERRAIN, on_arrive(me, level, other) { level.sfx.play_once('button-press', me.cell); // Move all yellow tanks one tile in the direction of the pressing actor for (let i = level.actors.length - 1; i >= 0; i--) { let actor = level.actors[i]; // TODO generify somehow?? if (actor.type.name === 'tank_yellow') { level.attempt_step(actor, other.direction); } } }, on_depart(me, level, other) { level.sfx.play_once('button-release', me.cell); }, }, button_green: { draw_layer: LAYER_TERRAIN, on_arrive(me, level, other) { level.sfx.play_once('button-press', me.cell); // Swap green floors and walls // TODO could probably make this more compact for undo purposes for (let row of level.cells) { for (let cell of row) { for (let tile of cell) { if (tile.type.name === 'green_floor') { level.transmute_tile(tile, 'green_wall'); } else if (tile.type.name === 'green_wall') { level.transmute_tile(tile, 'green_floor'); } else if (tile.type.name === 'green_chip') { level.transmute_tile(tile, 'green_bomb'); } else if (tile.type.name === 'green_bomb') { level.transmute_tile(tile, 'green_chip'); } } } } }, on_depart(me, level, other) { level.sfx.play_once('button-release', me.cell); }, }, button_brown: { draw_layer: LAYER_TERRAIN, connects_to: 'trap', connect_order: 'forward', on_ready(me, level) { // Inform the trap of any actors that start out holding us down let trap = me.connection; if (! (trap && trap.cell)) return; for (let tile of me.cell) { if (tile.type.is_actor) { trap.type.add_press_ready(trap, level); } } }, on_arrive(me, level, other) { level.sfx.play_once('button-press', me.cell); let trap = me.connection; if (trap && trap.cell) { trap.type.add_press(trap, level); } }, on_depart(me, level, other) { level.sfx.play_once('button-release', me.cell); let trap = me.connection; if (trap && trap.cell) { trap.type.remove_press(trap, level); } }, }, button_red: { draw_layer: LAYER_TERRAIN, connects_to: 'cloner', connect_order: 'forward', on_arrive(me, level, other) { level.sfx.play_once('button-press', me.cell); let cloner = me.connection; if (cloner && cloner.cell) { cloner.type.activate(cloner, level); } }, on_depart(me, level, other) { level.sfx.play_once('button-release', me.cell); }, }, button_orange: { draw_layer: LAYER_TERRAIN, connects_to: new Set(['flame_jet_off', 'flame_jet_on']), connect_order: 'diamond', // Both stepping on and leaving the button have the same effect: toggle the state of the // connected flame jet _toggle_flame_jet(me, level, other) { let jet = me.connection; if (jet && jet.cell) { jet.type.activate(jet, level); } }, on_arrive(me, level, other) { level.sfx.play_once('button-press', me.cell); me.type._toggle_flame_jet(me, level, other); }, on_depart(me, level, other) { level.sfx.play_once('button-release', me.cell); me.type._toggle_flame_jet(me, level, other); }, }, button_pink: { draw_layer: LAYER_TERRAIN, is_power_source: true, get_emitting_edges(me, level) { // We emit current as long as there's an actor fully on us if (me.cell.some(tile => tile.type.is_actor && tile.movement_cooldown === 0)) { return me.wire_directions; } else { return 0; } }, on_arrive(me, level, other) { level.sfx.play_once('button-press', me.cell); }, on_depart(me, level, other) { level.sfx.play_once('button-release', me.cell); }, }, button_black: { // TODO not implemented draw_layer: LAYER_TERRAIN, is_power_source: true, get_emitting_edges(me, level) { // We emit current as long as there's NOT an actor fully on us if (! me.cell.some(tile => tile.type.is_actor && tile.movement_cooldown === 0)) { return me.wire_directions; } else { return 0; } }, }, button_gray: { // TODO only partially implemented draw_layer: LAYER_TERRAIN, on_arrive(me, level, other) { level.sfx.play_once('button-press', me.cell); for (let x = Math.max(0, me.cell.x - 2); x <= Math.min(level.width - 1, me.cell.x + 2); x++) { for (let y = Math.max(0, me.cell.y - 2); y <= Math.min(level.height - 1, me.cell.y + 2); y++) { let cell = level.cells[y][x]; // TODO wait is this right if (cell === me.cell) continue; for (let tile of cell) { if (tile.type.on_gray_button) { tile.type.on_gray_button(tile, level); } } } } }, on_depart(me, level, other) { level.sfx.play_once('button-release', me.cell); }, }, // Logic gates, all consolidated into a single tile type logic_gate: { // gate_type: not, and, or, xor, nand, latch-cw, latch-ccw, counter, bogus _gate_types: { not: ['out', null, 'in1', null], and: ['out', 'in2', null, 'in1'], or: [], xor: [], nand: [], 'latch-cw': [], 'latch-ccw': [], }, draw_layer: LAYER_TERRAIN, is_power_source: true, get_emitting_edges(me, level) { if (me.gate_type === 'and') { let vars = {}; let out_bit = 0; let dir = me.direction; for (let name of me.type._gate_types[me.gate_type]) { let dirinfo = DIRECTIONS[dir]; if (name === 'out') { out_bit |= dirinfo.bit; } else if (name) { vars[name] = (me.cell.powered_edges & dirinfo.bit) !== 0; } dir = dirinfo.right; } if (vars.in1 && vars.in2) { return out_bit; } else { return 0; } } else { return 0; } }, visual_state(me) { return me.gate_type; }, }, // Time alteration stopwatch_bonus: { draw_layer: LAYER_ITEM, blocks_monsters: true, blocks_blocks: true, on_arrive(me, level, other) { if (other.type.is_player) { level.remove_tile(me); level.adjust_timer(+10); } }, }, stopwatch_penalty: { draw_layer: LAYER_ITEM, blocks_monsters: true, blocks_blocks: true, on_arrive(me, level, other) { if (other.type.is_player) { level.remove_tile(me); level.adjust_timer(-10); } }, }, stopwatch_toggle: { draw_layer: LAYER_ITEM, blocks_monsters: true, blocks_blocks: true, on_arrive(me, level, other) { if (other.type.is_player) { level.pause_timer(); } }, }, // Critters bug: { draw_layer: LAYER_ACTOR, is_actor: true, is_monster: true, blocks_monsters: true, blocks_blocks: true, movement_mode: 'follow-left', movement_speed: 4, }, paramecium: { draw_layer: LAYER_ACTOR, is_actor: true, is_monster: true, blocks_monsters: true, blocks_blocks: true, movement_mode: 'follow-right', movement_speed: 4, }, ball: { draw_layer: LAYER_ACTOR, is_actor: true, is_monster: true, blocks_monsters: true, blocks_blocks: true, movement_mode: 'bounce', movement_speed: 4, }, walker: { draw_layer: LAYER_ACTOR, is_actor: true, is_monster: true, blocks_monsters: true, blocks_blocks: true, movement_mode: 'bounce-random', movement_speed: 4, }, tank_blue: { draw_layer: LAYER_ACTOR, is_actor: true, is_monster: true, blocks_monsters: true, blocks_blocks: true, movement_mode: 'forward', movement_speed: 4, }, tank_yellow: { draw_layer: LAYER_ACTOR, is_actor: true, is_monster: true, blocks_monsters: true, blocks_blocks: true, pushes: { dirt_block: true, ice_block: true, directional_block: true, }, movement_speed: 4, }, blob: { draw_layer: LAYER_ACTOR, is_actor: true, is_monster: true, blocks_monsters: true, blocks_blocks: true, movement_mode: 'random', movement_speed: 8, }, teeth: { draw_layer: LAYER_ACTOR, is_actor: true, is_monster: true, blocks_monsters: true, blocks_blocks: true, movement_mode: 'pursue', movement_speed: 4, uses_teeth_hesitation: true, }, fireball: { draw_layer: LAYER_ACTOR, is_actor: true, is_monster: true, blocks_monsters: true, blocks_blocks: true, movement_mode: 'turn-right', movement_speed: 4, ignores: new Set(['fire', 'flame_jet_on']), }, glider: { draw_layer: LAYER_ACTOR, is_actor: true, is_monster: true, blocks_monsters: true, blocks_blocks: true, movement_mode: 'turn-left', movement_speed: 4, ignores: new Set(['water']), }, ghost: { draw_layer: LAYER_ACTOR, is_actor: true, is_monster: true, blocks_monsters: true, blocks_blocks: true, movement_mode: 'turn-right', movement_speed: 4, // TODO ignores /most/ walls. collision is basically completely different. has a regular inventory, except red key. good grief }, floor_mimic: { draw_layer: LAYER_ACTOR, is_actor: true, is_monster: true, blocks_monsters: true, blocks_blocks: true, // TODO not like teeth; always pursues // TODO takes 3 turns off! movement_mode: 'pursue', movement_speed: 4, }, rover: { // TODO this guy is a nightmare // TODO pushes blocks apparently?? draw_layer: LAYER_ACTOR, is_actor: true, is_monster: true, blocks_monsters: true, blocks_blocks: true, can_reveal_walls: true, movement_mode: 'random', movement_speed: 4, }, // Keys, whose behavior varies key_red: { // TODO Red key can ONLY be picked up by players (and doppelgangers), no other actor that // has an inventory draw_layer: LAYER_ITEM, is_item: true, is_key: true, }, key_blue: { // Blue key is picked up by dirt blocks and all monsters, including those that don't have an // inventory normally draw_layer: LAYER_ITEM, is_item: true, is_key: true, on_arrive(me, level, other) { // Call it... everything except ice and directional blocks? These rules are weird. // Note that the game itself normally handles picking items up, so we only get here for // actors who aren't supposed to have an inventory // TODO make this a... flag? i don't know? // TODO major difference from lynx... if (other.type.name !== 'ice_block' && other.type.name !== 'directional_block') { level.attempt_take(other, me); } }, }, key_yellow: { draw_layer: LAYER_ITEM, is_item: true, is_key: true, blocks_monsters: true, blocks_blocks: true, }, key_green: { draw_layer: LAYER_ITEM, is_item: true, is_key: true, blocks_monsters: true, blocks_blocks: true, }, // Boots // TODO note: ms allows blocks to pass over tools cleats: { draw_layer: LAYER_ITEM, is_item: true, is_tool: true, blocks_monsters: true, blocks_blocks: true, item_ignores: new Set(['ice', 'ice_nw', 'ice_ne', 'ice_sw', 'ice_se']), }, suction_boots: { draw_layer: LAYER_ITEM, is_item: true, is_tool: true, blocks_monsters: true, blocks_blocks: true, item_ignores: new Set([ 'force_floor_n', 'force_floor_s', 'force_floor_e', 'force_floor_w', 'force_floor_all', ]), }, fire_boots: { draw_layer: LAYER_ITEM, is_item: true, is_tool: true, blocks_monsters: true, blocks_blocks: true, item_ignores: new Set(['fire', 'flame_jet_on']), }, flippers: { draw_layer: LAYER_ITEM, is_item: true, is_tool: true, blocks_monsters: true, blocks_blocks: true, item_ignores: new Set(['water']), }, hiking_boots: { draw_layer: LAYER_ITEM, is_item: true, is_tool: true, blocks_monsters: true, blocks_blocks: true, // FIXME uhh these "ignore" that dirt and gravel block us, but they don't ignore the on_arrive, so, uhhhh }, // Other tools dynamite: { draw_layer: LAYER_ITEM, is_item: true, is_tool: true, blocks_monsters: true, blocks_blocks: true, // FIXME does a thing when dropped, but that isn't implemented at all yet }, bowling_ball: { // TODO not implemented, rolls when dropped, has an inventory, yadda yadda draw_layer: LAYER_ITEM, is_item: true, is_tool: true, blocks_monsters: true, blocks_blocks: true, }, xray_eye: { // TODO not implemented draw_layer: LAYER_ITEM, is_item: true, is_tool: true, blocks_monsters: true, blocks_blocks: true, }, helmet: { // TODO not implemented draw_layer: LAYER_ITEM, is_item: true, is_tool: true, blocks_monsters: true, blocks_blocks: true, }, railroad_sign: { draw_layer: LAYER_ITEM, is_item: true, is_tool: true, blocks_monsters: true, blocks_blocks: true, // FIXME this doesn't work any more, need to put it in railroad blocks impl item_ignores: new Set(['railroad']), }, foil: { // TODO not implemented draw_layer: LAYER_ITEM, is_item: true, is_tool: true, blocks_monsters: true, blocks_blocks: true, }, lightning_bolt: { // TODO not implemented draw_layer: LAYER_ITEM, is_item: true, is_tool: true, blocks_monsters: true, blocks_blocks: true, }, // Progression player: { draw_layer: LAYER_ACTOR, is_actor: true, is_player: true, has_inventory: true, can_reveal_walls: true, movement_speed: 4, pushes: { dirt_block: true, ice_block: true, directional_block: true, }, infinite_items: { key_green: true, }, visual_state: player_visual_state, }, player2: { draw_layer: LAYER_ACTOR, is_actor: true, is_player: true, has_inventory: true, can_reveal_walls: true, movement_speed: 4, ignores: new Set(['ice', 'ice_nw', 'ice_ne', 'ice_sw', 'ice_se']), pushes: { dirt_block: true, ice_block: true, directional_block: true, }, infinite_items: { key_yellow: true, }, visual_state: player_visual_state, }, chip: { draw_layer: LAYER_ITEM, is_chip: true, is_required_chip: true, blocks_monsters: true, blocks_blocks: true, on_arrive(me, level, other) { if (other.type.is_player) { level.collect_chip(); level.remove_tile(me); } }, }, chip_extra: { draw_layer: LAYER_ITEM, is_chip: true, blocks_monsters: true, blocks_blocks: true, on_arrive(me, level, other) { if (other.type.is_player) { level.collect_chip(); level.remove_tile(me); } }, }, score_10: { draw_layer: LAYER_ITEM, blocks_monsters: true, blocks_blocks: true, on_arrive(me, level, other) { if (other.type.is_player) { level.adjust_bonus(10); } level.remove_tile(me); }, }, score_100: { draw_layer: LAYER_ITEM, blocks_monsters: true, blocks_blocks: true, on_arrive(me, level, other) { if (other.type.is_player) { level.adjust_bonus(100); } level.remove_tile(me); }, }, score_1000: { draw_layer: LAYER_ITEM, blocks_monsters: true, blocks_blocks: true, on_arrive(me, level, other) { if (other.type.is_player) { level.adjust_bonus(1000); } level.remove_tile(me); }, }, score_2x: { draw_layer: LAYER_ITEM, blocks_monsters: true, blocks_blocks: true, on_arrive(me, level, other) { if (other.type.is_player) { level.adjust_bonus(0, 2); } level.remove_tile(me); }, }, hint: { draw_layer: LAYER_TERRAIN, is_hint: true, blocks_monsters: true, blocks_blocks: true, }, socket: { draw_layer: LAYER_TERRAIN, blocks_monsters: true, blocks_blocks: true, blocks(me, level, other) { return (level.chips_remaining > 0); }, on_arrive(me, level, other) { if (other.type.is_player && level.chips_remaining === 0) { level.sfx.play_once('socket'); level.transmute_tile(me, 'floor'); } }, }, exit: { draw_layer: LAYER_TERRAIN, blocks_monsters: true, blocks_blocks: true, on_arrive(me, level, other) { if (other.type.is_player) { level.win(); } }, }, // VFX splash: { draw_layer: LAYER_OVERLAY, is_actor: true, blocks_players: true, ttl: 6, }, explosion: { draw_layer: LAYER_OVERLAY, is_actor: true, blocks_players: true, ttl: 6, }, // Invalid tiles that appear in some CCL levels because community level // designers love to make nonsense bogus_player_win: { draw_layer: LAYER_TERRAIN, blocks_all: true, }, bogus_player_swimming: { draw_layer: LAYER_TERRAIN, blocks_all: true, }, bogus_player_drowned: { draw_layer: LAYER_TERRAIN, blocks_all: true, }, bogus_player_burned_fire: { draw_layer: LAYER_TERRAIN, blocks_all: true, }, bogus_player_burned: { draw_layer: LAYER_TERRAIN, blocks_all: true, }, }; // Tell them all their own names for (let [name, type] of Object.entries(TILE_TYPES)) { type.name = name; if (type.draw_layer === undefined || type.draw_layer !== Math.floor(type.draw_layer) || type.draw_layer >= 5) { console.error(`Tile type ${name} has a bad draw layer`); } } export default TILE_TYPES;