diff --git a/js/format-c2g.js b/js/format-c2g.js index 9f0d372..1701498 100644 --- a/js/format-c2g.js +++ b/js/format-c2g.js @@ -111,6 +111,16 @@ let modifier_wire = { }, }; +let modifier_color = { + _order: ['red', 'blue', 'yellow', 'green'], + decode(tile, modifier) { + tile.color = this._order[modifier % 4]; + }, + encode(tile) { + return this._order.indexOf(tile.color); + }, +}; + let arg_direction = { size: 1, decode(tile, dirbyte) { @@ -795,6 +805,7 @@ const TILE_ENCODING = { has_next: true, }, + // ------------------------------------------------------------------------------------------------ // LL-specific tiles 0xd0: { name: 'electrified_floor', @@ -826,11 +837,7 @@ const TILE_ENCODING = { has_next: true, extra_args: [arg_direction], }, - 0xd7: { - name: 'item_lock', - has_next: true, - is_extension: true, - }, + // 0xd7 0xd8: { name: 'dash_floor', is_extension: true, @@ -902,6 +909,23 @@ const TILE_ENCODING = { modifier: modifier_wire, is_extension: true, }, + 0xf1: { + name: 'sokoban_block', + has_next: true, + modifier: modifier_color, + extra_args: [arg_direction], + is_extension: true, + }, + 0xf2: { + name: 'sokoban_button', + modifier: modifier_color, + is_extension: true, + }, + 0xf3: { + name: 'sokoban_wall', + modifier: modifier_color, + is_extension: true, + }, }; const REVERSE_TILE_ENCODING = {}; for (let [tile_byte, spec] of Object.entries(TILE_ENCODING)) { diff --git a/js/game.js b/js/game.js index 2827f04..2addc48 100644 --- a/js/game.js +++ b/js/game.js @@ -521,6 +521,8 @@ export class Level extends LevelInterface { // If there's exactly one yellow teleporter when the level loads, it cannot be picked up let yellow_teleporter_count = 0; this.allow_taking_yellow_teleporters = false; + // Sokoban buttons function as a group + this.sokoban_buttons_unpressed = {}; for (let y = 0; y < this.height; y++) { let row = []; for (let x = 0; x < this.width; x++) { @@ -561,6 +563,10 @@ export class Level extends LevelInterface { this.allow_taking_yellow_teleporters = true; } } + else if (tile.type.name === 'sokoban_button') { + this.sokoban_buttons_unpressed[tile.color] = + (this.sokoban_buttons_unpressed[tile.color] ?? 0) + 1; + } } } } diff --git a/js/main-editor.js b/js/main-editor.js index fbe90a1..bb0515e 100644 --- a/js/main-editor.js +++ b/js/main-editor.js @@ -1613,6 +1613,18 @@ const EDITOR_PALETTE = [{ 'boulder', 'glass_block', 'logic_gate/diode', + 'sokoban_block/red', + 'sokoban_button/red', + 'sokoban_wall/red', + 'sokoban_block/blue', + 'sokoban_button/blue', + 'sokoban_wall/blue', + 'sokoban_block/green', + 'sokoban_button/green', + 'sokoban_wall/green', + 'sokoban_block/yellow', + 'sokoban_button/yellow', + 'sokoban_wall/yellow', ], }]; @@ -2247,6 +2259,18 @@ const EDITOR_TILE_DESCRIPTIONS = { name: "Glass block", desc: "Similar to a dirt block, but stores the first item it moves over, dropping it when destroyed and cloning it in a cloning machine. Has ice block/frame block collision. Turns into floor in water. Doesn't have dirt block immunities.", }, + sokoban_block: { + name: "Sokoban block", + desc: "Similar to a dirt block. Turns to colored floor in water. Can't pass over colored floor of a different color. Has no effect on sokoban buttons of a different color.", + }, + sokoban_button: { + name: "Sokoban button", + desc: "Changes sokoban walls of the same color to floor, but only while all buttons of the same color are held. Not affected by sokoban blocks of a different color.", + }, + sokoban_wall: { + name: "Sokoban wall", + desc: "Acts like wall. Turns to floor while all sokoban buttons of the same color are pressed.", + }, }; const SPECIAL_PALETTE_ENTRIES = { @@ -2262,7 +2286,7 @@ const SPECIAL_PALETTE_ENTRIES = { 'railroad/curve': { name: 'railroad', tracks: 1 << 0, track_switch: null, entered_direction: 'north' }, 'railroad/switch': { name: 'railroad', tracks: 0, track_switch: 0, entered_direction: 'north' }, 'logic_gate/not': { name: 'logic_gate', direction: 'north', gate_type: 'not' }, - 'logic_gate/diode': { name: 'logic_gate', direction: 'north', gate_type: 'diode' }, + 'logic_gate/diode': { name: 'logic_gate', direction: 'north', gate_type: 'diode' }, 'logic_gate/and': { name: 'logic_gate', direction: 'north', gate_type: 'and' }, 'logic_gate/or': { name: 'logic_gate', direction: 'north', gate_type: 'or' }, 'logic_gate/xor': { name: 'logic_gate', direction: 'north', gate_type: 'xor' }, @@ -2271,6 +2295,18 @@ const SPECIAL_PALETTE_ENTRIES = { 'logic_gate/latch-ccw': { name: 'logic_gate', direction: 'north', gate_type: 'latch-ccw' }, 'logic_gate/counter': { name: 'logic_gate', direction: 'north', gate_type: 'counter', memory: 0 }, 'circuit_block/xxx': { name: 'circuit_block', direction: 'south', wire_directions: 0xf }, + 'sokoban_block/red': { name: 'sokoban_block', color: 'red' }, + 'sokoban_button/red': { name: 'sokoban_button', color: 'red' }, + 'sokoban_wall/red': { name: 'sokoban_wall', color: 'red' }, + 'sokoban_block/blue': { name: 'sokoban_block', color: 'blue' }, + 'sokoban_button/blue': { name: 'sokoban_button', color: 'blue' }, + 'sokoban_wall/blue': { name: 'sokoban_wall', color: 'blue' }, + 'sokoban_block/yellow': { name: 'sokoban_block', color: 'yellow' }, + 'sokoban_button/yellow':{ name: 'sokoban_button', color: 'yellow' }, + 'sokoban_wall/yellow': { name: 'sokoban_wall', color: 'yellow' }, + 'sokoban_block/green': { name: 'sokoban_block', color: 'green' }, + 'sokoban_button/green': { name: 'sokoban_button', color: 'green' }, + 'sokoban_wall/green': { name: 'sokoban_wall', color: 'green' }, }; const _RAILROAD_ROTATED_LEFT = [3, 0, 1, 2, 5, 4]; const _RAILROAD_ROTATED_RIGHT = [1, 2, 3, 0, 5, 4]; diff --git a/js/tileset.js b/js/tileset.js index e7228e5..72494a4 100644 --- a/js/tileset.js +++ b/js/tileset.js @@ -1324,15 +1324,15 @@ export const LL_TILESET_LAYOUT = { duration: 20, all: [[8, 17], [9, 17], [10, 17], [9, 17]], }, - bowling_ball: [11, 16], + bowling_ball: [9, 19], rolling_ball: { __special__: 'animated', global: false, duration: 1, - north: [[12, 16], [13, 16], [11, 17], [11, 17], [11, 17], [14, 16], [15, 16], [11, 16]], - east: [[12, 17], [13, 17], [11, 17], [11, 17], [11, 17], [14, 17], [15, 17], [11, 16]], - south: [[15, 16], [14, 16], [11, 17], [11, 17], [11, 17], [13, 16], [12, 16], [11, 16]], - west: [[15, 17], [14, 17], [11, 17], [11, 17], [11, 17], [13, 17], [12, 17], [11, 16]], + north: [[14, 16], [15, 16], [13, 17], [13, 17], [13, 17], [11, 16], [12, 16], [13, 16]], + east: [[11, 17], [12, 17], [13, 17], [13, 17], [13, 17], [14, 17], [15, 17], [13, 16]], + south: [[12, 16], [11, 16], [13, 17], [13, 17], [13, 17], [15, 16], [14, 16], [13, 16]], + west: [[15, 17], [14, 17], [13, 17], [13, 17], [13, 17], [12, 17], [11, 17], [13, 16]], }, // LL bombs aren't animated bomb: [11, 18], @@ -1869,6 +1869,38 @@ export const LL_TILESET_LAYOUT = { wired: [17, 22], wired_cross: [18, 22], }, + sokoban_block: { + __special__: 'visual-state', + red: [26, 20], + blue: [26, 21], + yellow: [26, 22], + green: [26, 23], + }, + sokoban_button: { + __special__: 'visual-state', + red_released: [28, 20], + blue_released: [28, 21], + yellow_released: [28, 22], + green_released: [28, 23], + red_pressed: [29, 20], + blue_pressed: [29, 21], + yellow_pressed: [29, 22], + green_pressed: [29, 23], + }, + sokoban_wall: { + __special__: 'visual-state', + red: [30, 20], + blue: [30, 21], + yellow: [30, 22], + green: [30, 23], + }, + sokoban_floor: { + __special__: 'visual-state', + red: [31, 20], + blue: [31, 21], + yellow: [31, 22], + green: [31, 23], + }, rover: { __special__: 'rover', diff --git a/js/tiletypes.js b/js/tiletypes.js index 55c56f2..a039b56 100644 --- a/js/tiletypes.js +++ b/js/tiletypes.js @@ -272,18 +272,30 @@ const TILE_TYPES = { floor_custom_green: { layer: LAYERS.terrain, blocks_collision: COLLISION.ghost, + blocks(me, level, other) { + return (other.type.name === 'sokoban_block' && other.color !== 'green'); + }, }, floor_custom_pink: { layer: LAYERS.terrain, blocks_collision: COLLISION.ghost, + blocks(me, level, other) { + return (other.type.name === 'sokoban_block' && other.color !== 'red'); + }, }, floor_custom_yellow: { layer: LAYERS.terrain, blocks_collision: COLLISION.ghost, + blocks(me, level, other) { + return (other.type.name === 'sokoban_block' && other.color !== 'yellow'); + }, }, floor_custom_blue: { layer: LAYERS.terrain, blocks_collision: COLLISION.ghost, + blocks(me, level, other) { + return (other.type.name === 'sokoban_block' && other.color !== 'blue'); + }, }, wall: { layer: LAYERS.terrain, @@ -821,6 +833,15 @@ const TILE_TYPES = { level.transmute_tile(other, 'splash'); level.recalculate_circuitry_next_wire_phase = true; } + else if (other.type.name === 'sokoban_block') { + level.transmute_tile(me, ({ + red: 'floor_custom_pink', + blue: 'floor_custom_blue', + yellow: 'floor_custom_yellow', + green: 'floor_custom_green', + })[other.color]); + level.transmute_tile(other, 'splash'); + } else if (other.type.is_real_player) { level.fail('drowned', me, other); } @@ -1243,6 +1264,8 @@ const TILE_TYPES = { ice_block: true, frame_block: true, boulder: true, + glass_block: true, + sokoban_block: true, }, on_after_bumped(me, level, other) { // Fireballs melt ice blocks on regular floor FIXME and water! @@ -1276,6 +1299,7 @@ const TILE_TYPES = { frame_block: true, boulder: true, glass_block: true, + sokoban_block: true, }, on_clone(me, original) { me.arrows = new Set(original.arrows); @@ -1446,6 +1470,90 @@ const TILE_TYPES = { }, }, + // Sokoban blocks, buttons, and walls -- they each come in four colors, the buttons can be + // pressed by anything EXCEPT a sokoban block of the WRONG color, and the walls become floors + // only when ALL the buttons of the corresponding color are pressed + sokoban_block: { + layer: LAYERS.actor, + collision_mask: COLLISION.block_cc1, + blocks_collision: COLLISION.all, + item_pickup_priority: PICKUP_PRIORITIES.always, + is_actor: true, + is_block: true, + can_reverse_on_railroad: true, + movement_speed: 4, + populate_defaults(me) { + me.color = 'red'; + }, + visual_state(me) { + return me.color ?? 'red'; + }, + }, + sokoban_button: { + layer: LAYERS.terrain, + populate_defaults(me) { + me.color = 'red'; + }, + on_arrive(me, level, other) { + if (other.type.name === 'sokoban_block' && me.color !== other.color) + return; + level.sfx.play_once('button-press', me.cell); + + level.sokoban_buttons_unpressed[me.color] -= 1; + level._push_pending_undo(() => { + level.sokoban_buttons_unpressed[me.color] += 1; + }); + if (level.sokoban_buttons_unpressed[me.color] === 0) { + for (let cell of level.linear_cells) { + let terrain = cell.get_terrain(); + if (terrain.type.name === 'sokoban_wall' && terrain.color === me.color) { + level.transmute_tile(terrain, 'sokoban_floor'); + } + } + } + }, + on_depart(me, level, other) { + if (other.type.name === 'sokoban_block' && me.color !== other.color) + return; + level.sfx.play_once('button-release', me.cell); + + level.sokoban_buttons_unpressed[me.color] += 1; + level._push_pending_undo(() => { + level.sokoban_buttons_unpressed[me.color] -= 1; + }); + if (level.sokoban_buttons_unpressed[me.color] === 1) { + for (let cell of level.linear_cells) { + let terrain = cell.get_terrain(); + if (terrain.type.name === 'sokoban_floor' && terrain.color === me.color) { + level.transmute_tile(terrain, 'sokoban_wall'); + } + } + } + }, + visual_state(me) { + return (me.color ?? 'red') + '_' + button_visual_state(me); + }, + }, + sokoban_wall: { + layer: LAYERS.terrain, + blocks_collision: COLLISION.all_but_ghost, + populate_defaults(me) { + me.color = 'red'; + }, + visual_state(me) { + return me.color ?? 'red'; + }, + }, + sokoban_floor: { + layer: LAYERS.terrain, + populate_defaults(me) { + me.color = 'red'; + }, + visual_state(me) { + return me.color ?? 'red'; + }, + }, + // ------------------------------------------------------------------------------------------------ // Floor mechanisms cloner: { @@ -1613,6 +1721,14 @@ const TILE_TYPES = { let options = me.type._blob_mogrifications; level.transmute_tile(other, options[level.prng() % options.length]); } + else if (name === 'sokoban_block') { + level._set_tile_prop(other, 'color', ({ + red: 'blue', + blue: 'red', + yellow: 'green', + green: 'yellow', + })[other.color]); + } else { return; } @@ -2408,6 +2524,7 @@ const TILE_TYPES = { circuit_block: true, boulder: true, glass_block: true, + sokoban_block: true, }, decide_movement(me, level) { if (me.pending_decision) { @@ -2531,6 +2648,7 @@ const TILE_TYPES = { circuit_block: true, boulder: true, glass_block: true, + sokoban_block: true, }, on_ready(me, level) { me.current_emulatee = 0; @@ -2864,6 +2982,7 @@ const TILE_TYPES = { circuit_block: true, boulder: true, glass_block: true, + sokoban_block: true, }, infinite_items: { key_green: true, @@ -2888,6 +3007,7 @@ const TILE_TYPES = { circuit_block: true, boulder: true, glass_block: true, + sokoban_block: true, }, infinite_items: { key_yellow: true, @@ -2911,6 +3031,7 @@ const TILE_TYPES = { circuit_block: true, boulder: true, glass_block: true, + sokoban_block: true, }, infinite_items: { key_green: true, @@ -2938,6 +3059,7 @@ const TILE_TYPES = { circuit_block: true, boulder: true, glass_block: true, + sokoban_block: true, }, infinite_items: { key_yellow: true, diff --git a/tileset-lexy.png b/tileset-lexy.png index 9dc9858..ea0db60 100644 Binary files a/tileset-lexy.png and b/tileset-lexy.png differ diff --git a/tileset-src/tileset-lexy.aseprite b/tileset-src/tileset-lexy.aseprite index a6b5552..7158e9e 100644 Binary files a/tileset-src/tileset-lexy.aseprite and b/tileset-src/tileset-lexy.aseprite differ