diff --git a/js/game.js b/js/game.js index e463268..c7ccd3c 100644 --- a/js/game.js +++ b/js/game.js @@ -93,7 +93,7 @@ export class Tile { if (this.type.blocks && this.type.blocks(this, level, other, direction)) return true; - + if (other.type.blocked_by && other.type.blocked_by(other, level, this)) return true; @@ -116,7 +116,7 @@ export class Tile { return false; } - + slide_ignores(name) { if (this.type.slide_ignores && this.type.slide_ignores.has(name)) return true; @@ -614,7 +614,7 @@ export class Level extends LevelInterface { // Erase undo, in case any on_ready added to it (we don't want to undo initialization!) this.pending_undo = this.create_undo_entry(); } - + connect_button(connectable) { let cell = connectable.cell; let x = cell.x; @@ -673,11 +673,11 @@ export class Level extends LevelInterface { break; } } - + recalculate_circuitry(first_time = false, undoing = false) { // Build circuits out of connected wires // TODO document this idea - + if (!first_time) { for (let circuit of this.circuits) { for (let tile of circuit.tiles) { @@ -685,7 +685,7 @@ export class Level extends LevelInterface { } } } - + this.circuits = []; this.power_sources = []; let wired_outputs = new Set; @@ -714,7 +714,7 @@ export class Level extends LevelInterface { { wire_directions = actor.wire_directions; } - + if (! wire_directions && ! terrain.wire_tunnel_directions) { // No wires, not interesting... unless it's a logic gate, which defines its own // wires! We only care about outgoing ones here, on the off chance that they point @@ -812,7 +812,7 @@ export class Level extends LevelInterface { } this.wired_outputs = Array.from(wired_outputs); this.wired_outputs.sort((a, b) => this.coords_to_scalar(a.cell.x, a.cell.y) - this.coords_to_scalar(b.cell.x, b.cell.y)); - + if (!first_time) { //update wireables for (var i = 0; i < this.width; ++i) @@ -826,7 +826,7 @@ export class Level extends LevelInterface { } } } - + if (!undoing) { this._push_pending_undo(() => this.undid_past_recalculate_circuitry = true); } @@ -1767,7 +1767,7 @@ export class Level extends LevelInterface { move_to(actor, goal_cell) { if (actor.cell === goal_cell) return; - + if (actor.type.on_starting_move) { actor.type.on_starting_move(actor, this); } @@ -1811,15 +1811,15 @@ export class Level extends LevelInterface { // Helmet disables this, do nothing } else if (actor.type.is_real_player && tile.type.is_monster) { - this.fail(tile.type.name, tile, actor); + this.kill_actor(actor, tile); } else if (actor.type.is_monster && tile.type.is_real_player) { - this.fail(actor.type.name, actor, tile); + this.kill_actor(tile, actor); } else if (actor.type.is_block && tile.type.is_real_player && ! actor.is_pulled) { // Note that blocks squish players if they move for ANY reason, even if pushed by // another player! The only exception is being pulled - this.fail('squished', actor, tile); + this.kill_actor(tile, actor, null, null, 'squished'); } if (tile.type.on_approach) { @@ -1852,7 +1852,7 @@ export class Level extends LevelInterface { this.player.movement_cooldown === this.player.movement_speed && ! actor.has_item('helmet') && ! this.player.has_item('helmet')) { - this.fail(actor.type.name, actor, this.player); + this.kill_actor(this.player, actor); } if (this.compat.tiles_react_instantly) { @@ -1865,7 +1865,7 @@ export class Level extends LevelInterface { if (actor.type.on_finishing_move) { actor.type.on_finishing_move(actor, this); } - + // Step on topmost things first -- notably, it's safe to step on water with flippers on top // TODO is there a custom order here similar to collision checking? for (let layer = LAYERS.MAX - 1; layer >= 0; layer--) { @@ -2105,11 +2105,16 @@ export class Level extends LevelInterface { this.remove_tile(dropping_actor); this.add_tile(tile, cell); if (! this.attempt_out_of_turn_step(tile, dropping_actor.direction)) { - // It was unable to move, so there's nothing we can do but destroy it - // TODO maybe blow it up with a nonblocking vfx? in cc2 it just vanishes - this.remove_tile(tile); + // It was unable to move; if it exploded, we have a special non-blocking VFX for + // that, but otherwise there's nothing we can do but erase it (as CC2 does) + if (tile.type.name === 'explosion') { + this.transmute_tile(tile, 'explosion_nb', true); + } + else { + this.remove_tile(tile); + } } - else { + if (tile.cell) { this.add_actor(tile); } this.add_tile(dropping_actor, cell); @@ -2130,7 +2135,7 @@ export class Level extends LevelInterface { this.recalculate_circuitry_next_wire_phase = false; force_next_wire_phase = true; } - + if (this.circuits.length === 0) return; @@ -2302,7 +2307,7 @@ export class Level extends LevelInterface { return; } } - + //same as above, but accepts multiple tiles *iter_tiles_in_reading_order_multiple(start_cell, names, reverse = false) { let i = this.coords_to_scalar(start_cell.x, start_cell.y); @@ -2422,7 +2427,7 @@ export class Level extends LevelInterface { } this._undo_entry(this.undo_buffer[this.undo_buffer_index]); this.undo_buffer[this.undo_buffer_index] = null; - + if (this.undid_past_recalculate_circuitry) { this.recalculate_circuitry_next_wire_phase = true; this.undid_past_recalculate_circuitry = false; @@ -2525,13 +2530,40 @@ export class Level extends LevelInterface { } kill_actor(actor, killer, animation_name = null, sfx = null, fail_reason = null) { - // FIXME use this everywhere, fail when it's a player, move on_death here if (actor.type.is_real_player) { - // FIXME move death here - this.fail(fail_reason, null, actor); + // Resurrect using the ankh tile, if possible + if (this.ankh_tile) { + let ankh_cell = this.ankh_tile.cell; + let existing_actor = ankh_cell.get_actor(); + if (! existing_actor) { + // FIXME water should still splash, etc + this.sfx.play_once('revive'); + + this._set_tile_prop(actor, 'movement_cooldown', null); + this._set_tile_prop(actor, 'movement_speed', null); + this.make_slide(actor, null); + this.move_to(actor, ankh_cell); + + this.transmute_tile(this.ankh_tile, 'floor'); + this.spawn_animation(ankh_cell, 'resurrection'); + let old_tile = this.ankh_tile; + this.ankh_tile = null; + this._push_pending_undo(() => { + this.ankh_tile = old_tile; + }); + return; + } + } + + // Otherwise, lose the game + this.fail(fail_reason || killer.type.name, null, actor); return; } + if (actor.type.on_death) { + actor.type.on_death(actor, this); + } + if (sfx) { this.sfx.play_once(sfx, actor.cell); } @@ -2550,30 +2582,6 @@ export class Level extends LevelInterface { if (player === null) { player = this.player; } - - // FIXME move to kill_actor - if (player != null && this.ankh_tile && reason !== 'time') { - let cell = this.ankh_tile.cell; - let actor = cell.get_actor(); - if (! actor) { - // FIXME water should still splash, etc - this.sfx.play_once('revive'); - - this._set_tile_prop(player, 'movement_cooldown', null); - this._set_tile_prop(player, 'movement_speed', null); - this.make_slide(player, null); - this.move_to(player, cell); - - this.transmute_tile(this.ankh_tile, 'floor'); - this.spawn_animation(cell, 'resurrection'); - let old_tile = this.ankh_tile; - this.ankh_tile = null; - this._push_pending_undo(() => { - this.ankh_tile = old_tile; - }); - return; - } - } if (reason === 'time') { this.sfx.play_once('timeup'); @@ -2693,8 +2701,8 @@ export class Level extends LevelInterface { this.add_actor(tile); } - transmute_tile(tile, name) { - if (tile.type.ttl) { + transmute_tile(tile, name, force = false) { + if (tile.type.ttl && ! force) { // If this is already an animation, don't turn it into a different one; this can happen // if a block is pushed onto a cell containing both a mine and slime, both of which try // to destroy it @@ -2732,10 +2740,7 @@ export class Level extends LevelInterface { } this._init_animation(tile); this._set_tile_prop(tile, 'previous_cell', null); - } - - if (old_type.on_death) { - old_type.on_death(tile, this); + this.make_slide(tile, null); } } diff --git a/js/tileset.js b/js/tileset.js index be10f7c..1e1972b 100644 --- a/js/tileset.js +++ b/js/tileset.js @@ -151,6 +151,7 @@ export const CC2_TILESET_LAYOUT = { wall_custom_blue: [15, 4], explosion: [[0, 5], [1, 5], [2, 5], [3, 5]], + explosion_nb: [[0, 5], [1, 5], [2, 5], [3, 5]], splash_slime: [[0, 5], [1, 5], [2, 5], [3, 5]], splash: [[4, 5], [5, 5], [6, 5], [7, 5]], flame_jet_off: [8, 5], @@ -900,6 +901,7 @@ export const TILE_WORLD_TILESET_LAYOUT = { bogus_player_burned_fire: [3, 4], bogus_player_burned: [3, 5], explosion: [3, 6], + explosion_nb: [3, 6], explosion_other: [3, 7], // TODO ??? // 3, 8 unused bogus_player_win: [3, 9], // TODO 10 and 11 too? does this animate? @@ -1971,6 +1973,7 @@ export const LL_TILESET_LAYOUT = { // VFX explosion: [[16, 26], [17, 26], [18, 26], [19, 26]], + explosion_nb: [[16, 26], [17, 26], [18, 26], [19, 26]], splash: [[16, 27], [17, 27], [18, 27], [19, 27]], splash_slime: [[16, 28], [17, 28], [18, 28], [19, 28]], fall: [[16, 29], [17, 29], [18, 29], [19, 29]], diff --git a/js/tiletypes.js b/js/tiletypes.js index f2f8f81..66160c1 100644 --- a/js/tiletypes.js +++ b/js/tiletypes.js @@ -820,11 +820,8 @@ const TILE_TYPES = { })[other.color]); level.transmute_tile(other, 'splash'); } - else if (other.type.is_real_player) { - level.fail('drowned', me, other); - } else { - level.transmute_tile(other, 'splash'); + level.kill_actor(other, me, 'splash', null, 'drowned'); } }, }, @@ -1027,11 +1024,8 @@ const TILE_TYPES = { if (other.type.name === 'dirt_block' || other.type.name === 'ice_block') { level.transmute_tile(me, 'floor'); } - else if (other.type.is_real_player) { - level.fail('slimed', me, other); - } else { - level.transmute_tile(other, 'splash_slime'); + level.kill_actor(other, me, 'splash_slime', null, 'slimed'); } }, }, @@ -1051,13 +1045,7 @@ const TILE_TYPES = { }, on_arrive(me, level, other) { level.remove_tile(me); - if (other.type.is_real_player) { - level.fail('exploded', me, other); - } - else { - level.sfx.play_once('bomb', me.cell); - level.transmute_tile(other, 'explosion'); - } + level.kill_actor(other, me, 'explosion', 'bomb', 'exploded'); }, }, hole: { @@ -1072,12 +1060,7 @@ const TILE_TYPES = { } }, on_arrive(me, level, other) { - if (other.type.is_real_player) { - level.fail('fell', me, other); - } - else { - level.transmute_tile(other, 'fall'); - } + level.kill_actor(other, me, 'fall', null, 'fell'); }, visual_state(me) { return (me && me.visual_state) ?? 'open'; @@ -1087,18 +1070,17 @@ const TILE_TYPES = { layer: LAYERS.terrain, on_depart(me, level, other) { level.spawn_animation(me.cell, 'puff'); - level.transmute_tile(me, 'hole'); if (other === level.player) { level.sfx.play_once('popwall', me.cell); } - }, - on_death(me, level) { - //update hole visual state - me.type.on_begin(me, level); - var one_south = level.cell(me.cell.x, me.cell.y + 1); - if (one_south !== null && one_south.get_terrain().type.name == 'hole') { - me.type.on_begin(one_south.get_terrain(), level); - } + + level.transmute_tile(me, 'hole'); + // Update hole visual state (note that me.type is hole now) + me.type.on_begin(me, level); + var one_south = level.cell(me.cell.x, me.cell.y + 1); + if (one_south && one_south.get_terrain().type.name === 'hole') { + me.type.on_begin(one_south.get_terrain(), level); + } }, }, thief_tools: { @@ -1358,13 +1340,7 @@ const TILE_TYPES = { is_required_chip: true, on_arrive(me, level, other) { level.remove_tile(me); - if (other.type.is_real_player) { - level.fail('exploded', me, other); - } - else { - level.sfx.play_once('bomb', me.cell); - level.transmute_tile(other, 'explosion'); - } + level.kill_actor(other, me, 'explosion', 'bomb', 'exploded'); }, // Not affected by gray buttons }, @@ -1508,7 +1484,17 @@ const TILE_TYPES = { actor._clone_release = true; // Wire activation allows the cloner to try every direction, searching clockwise for (let i = 0; i < (aggressive ? 4 : 1); i++) { - if (level.attempt_out_of_turn_step(actor, direction)) { + // If the actor successfully moves, replace it with a new clone. As a special case, + // bowling balls that immediately destroy something are also considered to have + // successfully exited + let success = level.attempt_out_of_turn_step(actor, direction); + if (! success && actor.type.ttl && ! level.compat.cloned_bowling_balls_can_be_lost) { + success = true; + if (actor.type.layer === LAYERS.actor) { + level.transmute_tile(actor, 'explosion_nb', true); + } + } + if (success) { // Surprising edge case: if the actor immediately killed the player, do NOT // spawn a new template, since the move was actually aborted // FIXME this is inconsistent. the move was aborted because of an emergency @@ -1519,7 +1505,6 @@ const TILE_TYPES = { // FIXME add this underneath, just above the cloner, so the new actor is on top let new_template = new actor.constructor(type, direction); - // TODO maybe make a type method for this if (type.on_clone) { type.on_clone(new_template, actor); } @@ -1932,13 +1917,7 @@ const TILE_TYPES = { // Note that (dirt?) 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_real_player) { - level.fail('burned', me, other); - } - else { - level.sfx.play_once('bomb', me.cell); - level.transmute_tile(other, 'explosion'); - } + level.kill_actor(other, me, 'explosion', 'bomb', 'burned'); }, }, electrified_floor: { @@ -1952,13 +1931,7 @@ const TILE_TYPES = { if (! me.is_active) return; - if (other.type.is_real_player) { - level.fail('electrocuted', me, other); - } - else { - level.sfx.play_once('bomb', me.cell); - level.transmute_tile(other, 'explosion'); - } + level.kill_actor(other, me, 'explosion', 'bomb', 'electrocuted'); }, on_power(me, level) { level._set_tile_prop(me, 'is_active', true); @@ -1967,6 +1940,7 @@ const TILE_TYPES = { level._set_tile_prop(me, 'is_active', false); }, on_death(me, level) { + // FIXME i probably broke this lol //need to remove our wires since they're an implementation detail level._set_tile_prop(me, 'wire_directions', 0); level.recalculate_circuitry_next_wire_phase = true; @@ -2758,40 +2732,31 @@ const TILE_TYPES = { let actor = cell.get_actor(); let terrain = cell.get_terrain(); - let item = cell.get_item(); let removed_anything; for (let layer = LAYERS.MAX - 1; layer >= 0; layer--) { let tile = cell[layer]; if (! tile) continue; - if (tile.type.layer === LAYERS.terrain) { - // Terrain gets transmuted afterwards - } - else if (tile.type.is_real_player) { - // TODO it would be nice if i didn't have to special-case this every - // time - level.fail(me.type.name, me, tile); - } - else { - //newly appearing items (e.g. dropped by a glass block) are safe - if (tile.type.layer === LAYERS.item && tile !== item) { - continue; - } - // Everything else is destroyed - if (tile.type.on_death) { - tile.type.on_death(tile, level); - } - level.remove_tile(tile); - removed_anything = true; - } - + // Canopy protects everything else if (tile.type.name === 'canopy') { - // Canopy protects everything else actor = null; terrain = null; break; } + + // Terrain is transmuted afterwards; VFX are left alone; actors are killed + // after the loop (which also allows the glass block to safely drop an item) + if (tile.type.layer === LAYERS.terrain || + tile.type.layer === LAYERS.actor || + tile.type.layer === LAYERS.vfx) + { + continue; + } + + // Anything else is destroyed + level.remove_tile(tile); + removed_anything = true; } if (actor) { @@ -2807,25 +2772,29 @@ const TILE_TYPES = { } else if (terrain) { // Anything other than these babies gets blown up and turned into floor - if (terrain.type.name === 'hole') { - //do nothing + if (terrain.type.name === 'steel' || terrain.type.name === 'socket' || + terrain.type.name === 'logic_gate' || terrain.type.name === 'floor' || + terrain.type.name === 'hole' || terrain.type.name === 'floor_ankh') + { + // do nothing } else if (terrain.type.name === 'cracked_floor') { level.transmute_tile(terrain, 'hole'); removed_anything = true; } - else if (!( - terrain.type.name === 'steel' || terrain.type.name === 'socket' || - terrain.type.name === 'logic_gate' || terrain.type.name === 'floor')) - { + else { level.transmute_tile(terrain, 'floor'); removed_anything = true; } } - // TODO maybe add a vfx nonblocking explosion - if (removed_anything && ! cell.get_actor()) { - level.spawn_animation(cell, 'explosion'); + if (removed_anything || actor) { + if (actor) { + level.kill_actor(actor, me, 'explosion'); + } + else { + level.spawn_animation(cell, cell.get_actor() ? 'explosion_nb' : 'explosion'); + } } } } @@ -2849,6 +2818,7 @@ const TILE_TYPES = { is_monster: true, can_reveal_walls: true, collision_mask: COLLISION.bowling_ball, + blocks_collision: COLLISION.bowling_ball, item_pickup_priority: PICKUP_PRIORITIES.normal, // FIXME do i start moving immediately when dropped, or next turn? movement_speed: 4, @@ -2856,38 +2826,22 @@ const TILE_TYPES = { return [me.direction]; }, on_approach(me, level, other) { - // Blow up anything that runs into us... unless we're on a cloner - // FIXME there are other cases where this won't be right; this shouldn't happen if the - // cell blocks the actor, but i don't have a callback for that? - if (me.cell.has('cloner')) - return; - if (other.type.is_real_player) { - level.fail(me.type.name, me, other); - } - else { - level.transmute_tile(other, 'explosion'); - } - level.sfx.play_once('bomb', me.cell); - level.transmute_tile(me, 'explosion'); + // Blow up anything that runs into us + level.kill_actor(other, me, 'explosion'); + level.kill_actor(me, me, 'explosion', 'bomb'); }, on_blocked(me, level, direction, obstacle) { // Blow up anything we run into if (obstacle && obstacle.type.is_actor) { - if (obstacle.type.is_real_player) { - level.fail(me.type.name, me, obstacle); - } - else { - level.transmute_tile(obstacle, 'explosion'); - } + level.kill_actor(obstacle, me, 'explosion'); } - else if (me.slide_mode) { - // Sliding bowling balls don't blow up if they hit a regular wall + else if (me.slide_mode || me._clone_release) { + // Sliding bowling balls don't blow up if they hit a regular wall, and neither do + // bowling balls in the process of being released from a cloner return; } level.sfx.play_once('bomb', me.cell); level.transmute_tile(me, 'explosion'); - // Remove our slide mode so we don't attempt to bounce if on ice - level.make_slide(me, null); }, }, xray_eye: { @@ -3205,6 +3159,14 @@ const TILE_TYPES = { level.remove_tile(me); }, }, + // Non-blocking explosion used for better handling edge cases with dynamite and bowling balls, + // without changing gameplay + explosion_nb: { + layer: LAYERS.vfx, + is_actor: true, + collision_mask: 0, + ttl: 16, + }, // Used as an easy way to show an invisible wall when bumped wall_invisible_revealed: { layer: LAYERS.vfx,