From 63609ba77e64c5f7d322c0b1c62527bb6a08dabd Mon Sep 17 00:00:00 2001 From: "Eevee (Evelyn Woods)" Date: Mon, 8 Mar 2021 23:53:52 -0700 Subject: [PATCH] Fix a few more Lynx compat issues --- js/defs.js | 8 +++++ js/game.js | 87 ++++++++++++++++++++++++++++++------------------- js/tiletypes.js | 51 +++++++++++++++++++---------- 3 files changed, 95 insertions(+), 51 deletions(-) diff --git a/js/defs.js b/js/defs.js index 3fc7b16..ae77e0f 100644 --- a/js/defs.js +++ b/js/defs.js @@ -178,6 +178,10 @@ export const COMPAT_FLAGS = [ key: 'traps_like_lynx', label: "Traps eject faster, and even when already open", rulesets: new Set(['lynx']), +}, { + key: 'blue_floors_vanish_on_arrive', + label: "Fake blue walls vanish on arrival", + rulesets: new Set(['lynx']), }, // Items @@ -217,6 +221,10 @@ export const COMPAT_FLAGS = [ key: 'tanks_teeth_push_ice_blocks', label: "Ice blocks emulate pgchip rules", rulesets: new Set(['ms']), +}, { + key: 'allow_pushing_blocks_off_faux_walls', + label: "Blocks may be pushed off of blue (fake), invisible, and revealing walls", + rulesets: new Set(['lynx']), }, { key: 'emulate_spring_mining', label: "Spring mining is possible", diff --git a/js/game.js b/js/game.js index c7ccd3c..8f850bd 100644 --- a/js/game.js +++ b/js/game.js @@ -134,7 +134,7 @@ export class Tile { return false; } - can_push(tile, direction) { + can_push(tile, direction, level) { // This tile already has a push queued, sorry if (tile.pending_push) return false; @@ -158,7 +158,7 @@ export class Tile { // Need to explicitly check this here, otherwise you could /attempt/ to push a block, // which would fail, but it would still change the block's direction // XXX this expects to take a level but it only matters with push_mode === 'push' - return tile.cell.try_leaving(tile, direction); + return tile.cell.try_leaving(tile, direction, level); } can_pull(tile, direction) { @@ -261,7 +261,7 @@ export class Cell extends Array { if (thin_walls && thin_walls.type.blocks_leaving && thin_walls.type.blocks_leaving(thin_walls, actor, direction)) { blocker = thin_walls; } - else if (terrain.type.traps && terrain.type.traps(terrain, actor)) { + else if (terrain.type.traps && terrain.type.traps(terrain, level, actor)) { blocker = terrain; } else if (terrain.type.blocks_leaving && terrain.type.blocks_leaving(terrain, actor, direction)) { @@ -302,6 +302,7 @@ export class Cell extends Array { // It seems the order is thus: canopy + thin wall; terrain; actor; item. Which is the usual // ordering from the top down, except that terrain is checked before actors. Really, the // ordering is from "outermost" to "innermost", which makes physical sense. + let still_blocked = false; for (let layer of [ LAYERS.canopy, LAYERS.thin_wall, LAYERS.terrain, LAYERS.swivel, LAYERS.actor, LAYERS.item_mod, LAYERS.item]) @@ -310,6 +311,7 @@ export class Cell extends Array { if (! tile) continue; + let original_name = tile.type.name; // TODO check ignores here? if (tile.type.on_bumped) { tile.type.on_bumped(tile, level, actor); @@ -325,7 +327,7 @@ export class Cell extends Array { if (push_mode === null) return false; - if (actor.can_push(tile, direction) || ( + if (actor.can_push(tile, direction, level) || ( level.compat.tanks_teeth_push_ice_blocks && tile.type.name === 'ice_block' && (actor.type.name === 'teeth' || actor.type.name === 'teeth_timid' || actor.type.name === 'tank_blue') )) { @@ -338,6 +340,13 @@ export class Cell extends Array { if (actor.type.on_blocked) { actor.type.on_blocked(actor, level, direction, tile); } + // Lynx (or at least TW?) allows pushing blocks off of particular wall types + if (level.compat.allow_pushing_blocks_off_faux_walls && + ['fake_wall', 'wall_invisible', 'wall_appearing'].includes(original_name)) + { + still_blocked = true; + continue; + } } return false; } @@ -417,7 +426,7 @@ export class Cell extends Array { return false; } - return true; + return ! still_blocked; } // Special railroad ability: change the direction we attempt to leave @@ -1271,6 +1280,14 @@ export class Level extends LevelInterface { terrain.type.on_stand(terrain, this, actor); } } + // Lynx gives everything in an open trap an extra cooldown, which makes things walk into + // open traps at double speed and does weird things to the ejection timing + if (this.compat.traps_like_lynx) { + let terrain = actor.cell.get_terrain(); + if (terrain && terrain.type.name === 'trap' && terrain.presses > 0) { + this._do_extra_cooldown(actor); + } + } if (actor.just_stepped_on_teleporter) { this.attempt_teleport(actor); } @@ -1336,25 +1353,27 @@ export class Level extends LevelInterface { } // Strip out any destroyed actors from the acting order - if (! this.compat.reuse_actor_slots) { - // FIXME this is O(n), where n is /usually/ small, but i still don't love it. not strictly - // necessary, either; maybe only do it every few tics? - let p = 0; - for (let i = 0, l = this.actors.length; i < l; i++) { - let actor = this.actors[i]; - if (actor.cell) { - if (p !== i) { - this.actors[p] = actor; - } - p++; - } - else { - let local_p = p; - this._push_pending_undo(() => this.actors.splice(local_p, 0, actor)); + // FIXME this is O(n), where n is /usually/ small, but i still don't love it. not strictly + // necessary, either; maybe only do it every few tics? + let p = 0; + for (let i = 0, l = this.actors.length; i < l; i++) { + let actor = this.actors[i]; + if (actor.cell || ( + // Don't strip out actors under Lynx, where slots were reused -- unless they're VFX, + // which aren't in the original game and thus are exempt + this.compat.reuse_actor_slots && actor.type.layer !== LAYERS.vfx)) + { + if (p !== i) { + this.actors[p] = actor; } + p++; + } + else { + let local_p = p; + this._push_pending_undo(() => this.actors.splice(local_p, 0, actor)); } - this.actors.length = p; } + this.actors.length = p; // Advance the clock // TODO i suspect cc2 does this at the beginning of the tic, but even if you've won? if you @@ -1574,7 +1593,7 @@ export class Level extends LevelInterface { } if (forced_only) return; - if (terrain.type.traps && terrain.type.traps(terrain, actor)) { + if (terrain.type.traps && terrain.type.traps(terrain, this, actor)) { // An actor in a cloner or a closed trap can't turn // TODO because of this, if a tank is trapped when a blue button is pressed, then // when released, it will make one move out of the trap and /then/ turn around and @@ -1665,9 +1684,7 @@ export class Level extends LevelInterface { // Try to move the given actor one tile in the given direction and update their cooldown. // Return true if successful. - // ('frameskip' is an absolute number of frames subtracted from the normal speed, only used for - // Lynx's odd trap ejection behavior.) - attempt_step(actor, direction, frameskip = 0) { + attempt_step(actor, direction) { // In mid-movement, we can't even change direction! if (actor.movement_cooldown > 0) return false; @@ -1713,7 +1730,7 @@ export class Level extends LevelInterface { let orig_cell = actor.cell; this._set_tile_prop(actor, 'previous_cell', orig_cell); - let duration = Math.max(3, speed * 3 - frameskip); + let duration = speed * 3; this._set_tile_prop(actor, 'movement_cooldown', duration); this._set_tile_prop(actor, 'movement_speed', duration); this.move_to(actor, goal_cell); @@ -1739,14 +1756,14 @@ export class Level extends LevelInterface { return true; } - attempt_out_of_turn_step(actor, direction, frameskip = 0) { + attempt_out_of_turn_step(actor, direction) { if (actor.slide_mode === 'turntable') { // Something is (e.g.) pushing a block that just landed on a turntable and is waiting to // slide out of it. Ignore the push direction and move in its current direction; // otherwise a player will push a block straight through, then turn, which sucks direction = actor.direction; } - let success = this.attempt_step(actor, direction, frameskip); + let success = this.attempt_step(actor, direction); if (success) { this._do_extra_cooldown(actor); } @@ -2653,8 +2670,9 @@ export class Level extends LevelInterface { } add_actor(actor) { - if (this.compat.reuse_actor_slots) { - // Place the new actor in the first slot taken up by a nonexistent one + if (this.compat.reuse_actor_slots && actor.type.layer !== LAYERS.vfx) { + // Place the new actor in the first slot taken up by a nonexistent one, but not VFX + // which aren't supposed to impact gameplay for (let i = 0, l = this.actors.length; i < l; i++) { let old_actor = this.actors[i]; if (old_actor !== this.player && ! old_actor.cell) { @@ -2677,9 +2695,12 @@ export class Level extends LevelInterface { let duration = tile.type.ttl; if (this.compat.force_lynx_animation_lengths) { // Lynx animation duration is 12 tics, but it drops one if necessary to make the - // animation end on an even tic (???) and that takes step parity into account - // because I guess it uses the global clock (?????????????????) - duration = (12 - (this.tic_counter + this.step_parity) % 1) * 3; + // animation end on an odd tic (???) and that takes step parity into account + // because I guess it uses the global clock (?????????????????). Also, unlike CC2, Lynx + // animations are removed once their cooldown goes BELOW zero, so to simulate that we + // make the animation one tic longer. + // XXX wait am i sure that cc2 doesn't work that way too? + duration = (12 + (this.tic_counter + this.step_parity) % 2) * 3; } this._set_tile_prop(tile, 'movement_speed', duration); this._set_tile_prop(tile, 'movement_cooldown', duration); diff --git a/js/tiletypes.js b/js/tiletypes.js index 6fb4453..fe41bbb 100644 --- a/js/tiletypes.js +++ b/js/tiletypes.js @@ -407,13 +407,22 @@ const TILE_TYPES = { fake_floor: { layer: LAYERS.terrain, blocks_collision: COLLISION.block_cc1 | COLLISION.monster_general, + reveal(me, level, other) { + level.spawn_animation(me.cell, 'puff'); + level.transmute_tile(me, 'floor'); + if (other === level.player) { + level.sfx.play_once('fake-floor', me.cell); + } + }, on_bumped(me, level, other) { - if (other.type.can_reveal_walls) { - level.spawn_animation(me.cell, 'puff'); - level.transmute_tile(me, 'floor'); - if (other === level.player) { - level.sfx.play_once('fake-floor', me.cell); - } + if (other.type.can_reveal_walls && ! level.compat.blue_floors_vanish_on_arrive) { + this.reveal(me, level, other); + } + }, + on_arrive(me, level, other) { + // In Lynx, these disappear only when you step on them + if (level.compat.blue_floors_vanish_on_arrive) { + this.reveal(me, level, other); } }, }, @@ -1465,8 +1474,8 @@ const TILE_TYPES = { on_ready(me, level) { me.arrows = me.arrows ?? 0; }, - traps(me, actor) { - return ! actor._clone_release; + traps(me, level, other) { + return ! other._clone_release; }, activate(me, level, aggressive = false) { let actor = me.cell.get_actor(); @@ -1532,10 +1541,9 @@ const TILE_TYPES = { } }, on_arrive(me, level, other) { - // Lynx (not cc2): open traps immediately eject their contents on arrival, if possible, - // and also do it slightly faster + // Lynx (not cc2): open traps immediately eject their contents on arrival, if possible if (level.compat.traps_like_lynx) { - level.attempt_out_of_turn_step(other, other.direction, 3); + level.attempt_out_of_turn_step(other, other.direction); } }, add_press_ready(me, level, other) { @@ -1544,17 +1552,13 @@ const TILE_TYPES = { }, add_press(me, level, is_wire = false) { level._set_tile_prop(me, 'presses', me.presses + 1); - // TODO weird cc2 case that may or may not be a bug: actors aren't ejected if the trap - // opened because of wiring if (me.presses === 1 && ! is_wire) { // Free any actor on us, if we went from 0 to 1 presses (i.e. closed to open) let actor = me.cell.get_actor(); if (actor) { // Forcibly move anything released from a trap, which keeps it in sync with // whatever pushed the button - level.attempt_out_of_turn_step( - actor, actor.direction, - level.compat.traps_like_lynx ? 3 : 0); + level.attempt_out_of_turn_step(actor, actor.direction); } } }, @@ -1565,8 +1569,19 @@ const TILE_TYPES = { } }, // FIXME also doesn't trap ghosts, is that a special case??? - traps(me, actor) { - return ! me.presses && ! me._initially_open && actor.type.name !== 'ghost'; + traps(me, level, other) { + if (level.compat.traps_like_lynx) { + // Lynx traps don't actually track open vs closed; actors are just ejected by force + // by a separate pass at the end of the tic that checks what's on a brown button. + // That means a trap held open by a button at level start won't effectively be open + // if whatever's on the button moves within the first tic, a quirk that CCLXP2 #17 + // Double Trouble critically relies on! + // To fix this, assume that a trap can never be released on the first turn. + // FIXME that's not right since a block or immobile mob might be on a button... + if (level.tic_counter === 0) + return true; + } + return ! me.presses && ! me._initially_open && other.type.name !== 'ghost'; }, on_power(me, level) { // Treat being powered or not as an extra kind of brown button press